文章篇幅较长, 但是包含了 SpringBoot 可执行 jar 包从头到尾的原理, 请读者耐心观看. 同时文章是基于 SpringBoot-2.1.3 进行分析. 涉及的知识点主要包括 Maven 的生命周期以及自定义插件, JDK 提供关于 jar 包的工具类以及 Springboot 如何扩展, 最后是自定义类加载器.
spring-boot-maven-plugin
SpringBoot 的可执行 jar 包又称 fat jar , 是包含所有依赖的 jar 包, jar 包中嵌入了除 java 虚拟机以外的所有依赖, 是一个 all-in-one jar 包. 普通插件 maven-jar-plugin 生成的包和 spring-boot-maven-plugin 生成的包之间的直接区别, 是 fat jar 中主要增加了两部分, 第一部分是 lib 目录, 存放的是 Maven 依赖的 jar 包文件, 第二部分是 spring boot loader 相关的类.
fat jar 目录结构
├─BOOT-INF
│ ├─classes
│ └─lib
├─META-INF
│ ├─maven
│ ├─App.properties
│ ├─MANIFEST.MF
└─org
└─springframework
└─boot
└─loader
├─archive
├─data
├─jar
└─util
也就是说想要知道 fat jar 是如何生成的, 就必须知道 spring-boot-maven-plugin 工作机制, 而 spring-boot-maven-plugin 属于自定义插件, 因此我们又必须知道, Maven 的自定义插件是如何工作的
Maven 的自定义插件
Maven 拥有三套相互独立的生命周期: clean,default 和 site, 而每个生命周期包含一些 phase 阶段, 阶段是有顺序的, 并且后面的阶段依赖于前面的阶段. 生命周期的阶段 phase 与插件的目标 goal 相互绑定, 用以完成实际的构建任务.
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>repackage</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
repackage 目标对应的将执行到 org.springframework.boot.maven.RepackageMojo#execute, 该方法的主要逻辑是调用了 org.springframework.boot.maven.RepackageMojo#repackage
- private void repackage() throws MojoExecutionException {
- // 获取使用 maven-jar-plugin 生成的 jar, 最终的命名将加上. orignal 后缀
- Artifact source = getSourceArtifact();
- // 最终文件, 即 Fat jar
- File target = getTargetFile();
- // 获取重新打包器, 将重新打包成可执行 jar 文件
- Repackager repackager = getRepackager(source.getFile());
- // 查找并过滤项目运行时依赖的 jar
- Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
- getFilters(getAdditionalFilters()));
- // 将 artifacts 转换成 libraries
- Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
- getLog());
- try {
- // 提供 Spring Boot 启动脚本
- LaunchScript launchScript = getLaunchScript();
- // 执行重新打包逻辑, 生成最后 fat jar
- repackager.repackage(target, libraries, launchScript);
- }
- catch (IOException ex) {
- throw new MojoExecutionException(ex.getMessage(), ex);
- }
- // 将 source 更新成 xxx.jar.orignal 文件
- updateArtifact(source, target, repackager.getBackupFile());
- }
我们关心一下 org.springframework.boot.maven.RepackageMojo#getRepackager 这个方法, 知道 Repackager 是如何生成的, 也就大致能够推测出内在的打包逻辑.
- private Repackager getRepackager(File source) {
- Repackager repackager = new Repackager(source, this.layoutFactory);
- repackager.addMainClassTimeoutWarningListener(
- new LoggingMainClassTimeoutWarningListener());
- // 设置 main class 的名称, 如果不指定的话则会查找第一个包含 main 方法的类, repacke 最后将会设置 org.springframework.boot.loader.JarLauncher
- repackager.setMainClass(this.mainClass);
- if (this.layout != null) {
- getLog().info("Layout:" + this.layout);
- // 重点关心下 layout 最终返回了 org.springframework.boot.loader.tools.Layouts.Jar
- repackager.setLayout(this.layout.layout());
- }
- return repackager;
- }
- /**
- * Executable JAR layout.
- */
- public static class Jar implements RepackagingLayout {
- @Override
- public String getLauncherClassName() {
- return "org.springframework.boot.loader.JarLauncher";
- }
- @Override
- public String getLibraryDestination(String libraryName, LibraryScope scope) {
- return "BOOT-INF/lib/";
- }
- @Override
- public String getClassesLocation() {
- return "";
- }
- @Override
- public String getRepackagedClassesLocation() {
- return "BOOT-INF/classes/";
- }
- @Override
- public boolean isExecutable() {
- return true;
- }
- }
layout 我们可以将之翻译为文件布局, 或者目录布局, 代码一看清晰明了, 同时我们需要关注, 也是下一个重点关注对象 org.springframework.boot.loader.JarLauncher, 从名字推断, 这很可能是返回可执行 jar 文件的启动类.
MANIFEST.MF 文件内容
- Manifest-Version: 1.0
- Implementation-Title: oneday-auth-server
- Implementation-Version: 1.0.0-SNAPSHOT
- Archiver-Version: Plexus Archiver
- Built-By: oneday
- Implementation-Vendor-Id: com.oneday
- Spring-Boot-Version: 2.1.3.RELEASE
- Main-Class: org.springframework.boot.loader.JarLauncher
- Start-Class: com.oneday.auth.Application
- Spring-Boot-Classes: BOOT-INF/classes/
- Spring-Boot-Lib: BOOT-INF/lib/
- Created-By: Apache Maven 3.3.9
- Build-Jdk: 1.8.0_171
repackager 生成的 MANIFEST.MF 文件为以上信息, 可以看到两个关键信息 Main-Class 和 Start-Class. 我们可以进一步, 程序的启动入口并不是我们 SpringBoot 中定义的 main, 而是 JarLauncher#main, 而再在其中利用反射调用定义好的 Start-Class 的 main 方法
JarLauncher
重点类介绍
java.util.jar.JarFile
JDK 工具类提供的读取 jar 文件
org.springframework.boot.loader.jar.JarFile
Springboot-loader 继承 JDK 提供 JarFile 类
java.util.jar.JarEntry
DK 工具类提供的 ``jar``` 文件条目
org.springframework.boot.loader.jar.JarEntry
Springboot-loader 继承 JDK 提供 JarEntry 类
org.springframework.boot.loader.archive.Archive
Springboot 抽象出来的统一访问资源的层
JarFileArchivejar 包文件的抽象
ExplodedArchive 文件目录
这里重点描述一下 JarFile 的作用, 每个 JarFileArchive 都会对应一个 JarFile. 在构造的时候会解析内部结构, 去获取 jar 包里的各个文件或文件夹类. 我们可以看一下该类的注释.
/* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality.
* <ul>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
* on any directory entry.</li>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
* embedded JAR files (as long as their entry is not compressed).</li>
**/ </ul>
jar 里的资源分隔符是!/, 在 JDK 提供的 JarFile URL 只支持一个'!/', 而 Spring boot 扩展了这个协议, 让它支持多个'!/', 就可以表示 jar in jar,jar in directory,fat jar 的资源了.
自定义类加载机制
最基础: Bootstrap ClassLoader(加载 JDK 的 / lib 目录下的类)
次基础: Extension ClassLoader(加载 JDK 的 / lib/ext 目录下的类)
普通: Application ClassLoader(程序自己 classpath 下的类)
首先需要关注双亲委派机制很重要的一点是, 如果一个类可以被委派最基础的 ClassLoader 加载, 就不能让高层的 ClassLoader 加载, 这样是为了范围错误的引入了非 JDK 下但是类名一样的类. 其二, 如果在这个机制下, 由于 fat jar 中依赖的各个 jar 文件, 并不在程序自己 classpath 下, 也就是说, 如果我们采用双亲委派机制的话, 根本获取不到我们所依赖的 jar 包, 因此我们需要修改双亲委派机制的查找 class 的方法, 自定义类加载机制.
先简单的介绍 Springboot2 中 LaunchedURLClassLoader, 该类继承了 java.NET.URLClassLoader, 重写了 java.lang.ClassLoader#loadClass(java.lang.String, boolean), 然后我们再探讨他是如何修改双亲委派机制.
在上面我们讲到 Spring boot 支持多个'!/'以表示多个 jar, 而我们的问题在于, 如何解决查找到这多个 jar 包. 我们看一下 LaunchedURLClassLoader 的构造方法.
- public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
- super(urls, parent);
- }
urls 注释解释道 the URLs from which to load classes and resources, 即 fat jar 包依赖的所有类和资源, 将该 urls 参数传递给父类 java.NET.URLClassLoader, 由父类的 java.NET.URLClassLoader#findClass 执行查找类方法, 该类的查找来源即构造方法传递进来的 urls 参数
- //LaunchedURLClassLoader 的实现
- protected Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException {
- Handler.setUseFastConnectionExceptions(true);
- try {
- try {
- // 尝试根据类名去定义类所在的包, 即 java.lang.Package, 确保 jar in jar 里匹配的 manifest 能够和关联 // 的 package 关联起来
- definePackageIfNecessary(name);
- }
- catch (IllegalArgumentException ex) {
- // Tolerate race condition due to being parallel capable
- if (getPackage(name) == null) {
- // This should never happen as the IllegalArgumentException indicates
- // that the package has already been defined and, therefore,
- // getPackage(name) should not return null.
- // 这里异常表明, definePackageIfNecessary 方法的作用实际上是预先过滤掉查找不到的包
- throw new AssertionError("Package" + name + "has already been"
- + "defined but it could not be found");
- }
- }
- return super.loadClass(name, resolve);
- }
- finally {
- Handler.setUseFastConnectionExceptions(false);
- }
- }
方法 super.loadClass(name, resolve) 实际上会回到了 java.lang.ClassLoader#loadClass(java.lang.String, boolean), 遵循双亲委派机制进行查找类, 而 Bootstrap ClassLoader 和 Extension ClassLoader 将会查找不到 fat jar 依赖的类, 最终会来到 Application ClassLoader, 调用 java.NET.URLClassLoader#findClass
如何真正的启动
Springboot2 和 Springboot1 的最大区别在于, Springboo1 会新起一个线程, 来执行相应的反射调用逻辑, 而 SpringBoot2 则去掉了构建新的线程这一步. 方法是 org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader) 反射调用逻辑比较简单, 这里就不再分析, 比较关键的一点是, 在调用 main 方法之前, 将当前线程的上下文类加载器设置成 LaunchedURLClassLoader
- protected void launch(String[] args, String mainClass, ClassLoader classLoader)
- throws Exception {
- Thread.currentThread().setContextClassLoader(classLoader);
- createMainMethodRunner(mainClass, args, classLoader).run();
- }
- Demo
- public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
- JarFile.registerUrlProtocolHandler();
- // 构造 LaunchedURLClassLoader 类加载器, 这里使用了 2 个 URL, 分别对应 jar 包中依赖包 spring-boot-loader 和 spring-boot, 使用 "!/" 分开, 需要 org.springframework.boot.loader.jar.Handler 处理器处理
- LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(
- new URL[] {
- new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-loader-1.2.3.RELEASE.jar!/")
- , new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-2.1.3.RELEASE.jar!/")
- },
- Application.class.getClassLoader());
- // 加载类
- // 这 2 个类都会在第二步本地查找中被找出 (URLClassLoader 的 findClass 方法)
- classLoader.loadClass("org.springframework.boot.loader.JarLauncher");
- classLoader.loadClass("org.springframework.boot.SpringApplication");
- // 在第三步使用默认的加载顺序在 ApplicationClassLoader 中被找出
- classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");
- // SpringApplication.run(Application.class, args);
- }
总结
对于源码分析, 这次的较大收获则是不能一下子去追求弄懂源码中的每一步代码的逻辑, 即便我知道该方法的作用. 我们需要搞懂的是关键代码, 以及涉及到的知识点. 我从 Maven 的自定义插件开始进行追踪, 巩固了对 Maven 的知识点, 在这个过程中甚至了解到 JDK 对 jar 的读取是有提供对应的工具类. 最后最重要的知识点则是自定义类加载器. 整个代码下来并不是说代码究竟有多优秀, 而是要学习他因何而优秀.
作者: plz 叫我红领巾
来源: https://www.cnblogs.com/xxzhuang/p/11194559.html