Spring Boot Loader 源码分析

Spring Boot jar

我们之前就发现 Spring Boot 打包出来的 jar 解压后,有三个目录。

SpringBootJarDir

BOOT-INF

classes

当前工程编译好的结果文件,包含 src/main/java 和 src/main/resources 目录下的所有文件。

lib

当前工程依赖的所有 jar 文件(第三方 jar 包)。

META-INF

只有一个文件:MANIFEST.MF。

MANIFEST.MF-01

org

由 Spring Boot 提供的一堆字节码文件,入口类就在这里。

org package 是怎么来的

BOOT-INF 是通过编译工程文件,META-INF 是打包时自动生成,那么 org package 是怎么来的?

还记得我们之间配置的 gradle plugin 么?既然打包是由 org.springframework.boot 这个插件完成,那么答案肯定在这里。

根据 org 的目录结构,尝试增加一个依赖到工程里。

org.springframework.boot:spring-boot-loader (在开发阶段,因为有 gradle 插件的存在,原则上这个依赖是不引入的,这里是研究需要

再观察 spring-boot-loader.jar,发现和上面的 org package 一模一样。

spring-boot-loader

结论:说明 Spring Boot Gradle Plugin 在用 bootJar 任务对 Spring Boot 工程进行打包的时候,是把 spring-boot-loader.jar 这个 jar 解压,随后把解压内容放置在 Spring Boot 的应用 jar 里。而 spring-boot-loader.jar 这个依赖,肯定是在 Spring Boot Gradle Plugin 里被依赖的。

疑问:为什么不直接把这个依赖传递给实际的 Spring Boot 应用,而是采用这么麻烦的方式,把这个依赖的内容打包进实际的 Spring Boot 应用。

JarLauncher 启动类

根据 MANIFEST.MF 文件的描述。

Spring Boot 的启动类就是

而 Spring Boot 的应用主类是

源码

类关系图

JarLauncherClass

从图中可以看出来,最顶层的启动器是 Launcher,其次是 ExecutableArchiveLauncher。最终的实现分别两种模式,一个是 JarLauncher(针对于 jar 归档文件),另一个是 WarLauncher(针对于 war 归档文件)。从这里也就说明了为什么 Spring Boot 的应用可以通过 jar 和 war 两种方式运行。

源码分析

通过代码可以发现,实际程序的入口是在:

实际调用的 launch 方法是 Launcher.java 里的 launch 方法。

这里一共 3 个步骤,最重要的就是第 2 步 ClassLoader 类加载器。

registerUrlProtocolHandler

注册URL协议处理器,这个不重要,可以忽略。

classLoader

最重要的就是自定义类加载器这个阶段。

getClassPathArchives

返回所有符合条件的 jar 或 工程文件,并包装成一个类型为 Archive 的 List 对象。这里的符合条件是指在 BOOT-INF/lib/ 下的 jar 文件,和在 BOOT-INF/classes/ 下的所有工程文件,用来构建 classpath。

createArchive

定位当前执行的具体 jar 文件或者文件目录,通过磁盘上的绝对路径来定位,返回一个 Archive 对象。

getNestedArchives

返回与指定过滤器(EntryFilter)所匹配的嵌套归档文件。

isNestedArchive

EntryFilter 过滤器,判断 entry 所指定的文件(具体 jar 文件或者文件目录)是否满足条件 ,满足条件的应该添加到 classpath 里,每个指定的文件都会调用一次。

条件:在 BOOT-INF/classes/ 目录下的工程文件或者在 BOOT-INF/lib/ 目录下的第三方 jar 包。

注意:其实这里就是在判断,要执行的 jar 文件是否是按照 Spring Boot 特有的目录结构来放置工程文件,以及所依赖的第三方 jar 包。只有满足条件的工程文件或所依赖的第三方 jar 包才会进入下一步,也就是通过自定义类加载器来加载这些满足条件的文件,这里就需要 jar 文件规范相关知识。

postProcessClassPathArchives

是一个空方法,事后处理方法,回调方法。

createClassLoader

上面的所有方法,都是为了准备 List 对象,所有符合条件的 jar(BOOT-INF/lib/ )和工程文件(BOOT-INF/classes/ ),并包装成一个类型为 Archive 的 List 对象。

创建一个针对指定归档文件(Archive)的自定义类加载器,也就是用来加载 getClassPathArchives 所返回的集合(jar 或者 工程文件),应用类加载器(也可以叫做系统类加载器)是加载不了这些文件的,就必须自己创建一个新的类加载器,用来加载这些存在于自定义目录内的文件。

这个方法是把传入的 List 对象转撑一个 List 对象,URL 表示文件在磁盘上的绝对路径。

LaunchedURLClassLoader

Spring Boot 提供的自定义类加载器,urls 表示所有需要加载文件的 url(jar 文件的绝对路径),getClass().getClassLoader() 表示父加载器(也就是应用类加载器)。

注意:在创建一个类加载器的时候,一定要指定它的父加载器 getClass().getClassLoader(),这个父加载器其实就是应用类加载器。

这个方法创建一个针对指定归档文件(URL)的类加载器。

launch

最后阶段,通过反射来完成工程应用启动。

getMainClass

返回应该被加载的主类。从 Manifest 对象种,获取 Start-Class 属性,这个 Start-Class 是什么呢?

MANIFEST.MF 文件中定义 Start-Class: com.lichee.microservices.MicroservicesApplication

launch

通过给定的归档文件和全新的 classloader 来启动应用。

第一行代码把自定义的 classloader 设置到当前线程上下文类加载器,在默认情况下,当前线程上下文类加载器就是 AppClassLoader。通过这种方式就把当前线程上下文的默认类加载器换成了 Spring Boot 自定义的类加载器。

转换:AppClassLoader --> LaunchedURLClassLoader

现在是把类加载器放置进去,在未来某处肯定会从当前线程中取出这个上下文类加载器,然后进行类加载。

createMainMethodRunner

创建 MainMethodRunner,用于启动和加载应用。

其实这里的 classLoader 并没有用到。

MainMethodRunner

使用当前线程上下文类加载器,加载一个包含了 main 方法的主类,然后调用这个主类的 main 方法。主要看这个 run 方法。

run

当我们使用下面这个指令的时候,就会启动 Spring Boot 的应用,原理就在这个 run 方法。

Thread.currentThread().getContextClassLoader() 获取当前线程上下文类加载器,其实也就是获取我们之前已经设置好的 LaunchedURLClassLoader。之前从 MANIFEST.MF 文件中把 Start-Class 取出来,也就是我们的主类:com.lichee.microservices.MicroservicesApplication,通过 loadClass 方法就把主类加载到虚拟机中,于是得到主类的一个 Class 对象。

通过反射主类 Class 对象的方式,拿到主类的 main 方法。

通过反射方式直接调用 main 方法。

总结下来三个步骤:

  1. 通过当前线程上下文类加载器加载 Start-Class 定义的主类到虚拟机中,拿到 Class 对象

  2. 通过 Class 对象反射拿到 main 方法

  3. 通过反射执行 main 方法

思考

为什么 Spring Boot 规定入口类(被 @SpringBootApplication 注释的类)的启动方法是 main 方法?

通过上面的源码分析,不需要非要 main 方法才可以,在反射阶段,完全可以换成另外一个普通的实例方法,只要在反射的时候,更换方法名就可以。

原因在于为了方便 Spring Boot 应用开发阶段的运行、调试,但是类加载器不一样。

两种运行方式

Spring Boot 可以通过以下两种方式运行,好处就是便利了日常开发、调试、测试阶段。

但是类加载器会有明显的区别,修改项目代码,把类加载器打印出来。

jar

通过指令运行。从结果可以看出来是采用的 LaunchedURLClassLoader,也就是采用 Spring Boot 提供的全新类加载器。

LaunchedURLClassLoader

main

直接在开发阶段的 ide(eclipse、idea)里右键 run 方式运行。从结果可以看出来是采用的 AppClassLoader,也就是采用应用类加载器。

AppClassLoader

总结

打包机制

Spring Boot 打包机制,通过 gradle plugin 的 org.springframework.boot 插件对应用打成 jar 包,在 jar 包的根目录放置 spring-boot-loader.jar 这个 jar 包解压缩后的所有文件。自定义 BOOT-INF 和 META-INF 两个目录,BOOT-INF/classes 放置工程文件,BOOT-INF/lib 放置工程三方依赖,MANIFEST.MF 文件中定义 Start-Class 和 Main-Class 属性。

运行机制

Spring Boot 运行机制,首先应用加载器(系统加载器)加载 org.springframework.boot.loader.JarLauncher。在加载 JarLauncher 的同时,创建一个 Spring Boot 特有的类加载器 LaunchedURLClassLoader,用这个特有的类加载器加载 BOOT-INF 下的工程文件和三方依赖。最后通过反射调用 Start-Class 应用入口类的 main 方法启动应用程序。

上面所讲的是 jar 包形式运行,开发阶段直接在 ide 里右键 run 运行工程,则是直接调用系统的 AppClassLoader 类加载器。

优雅方式

Spring Boot 通过自定义类加载器这种方式,优雅的解决了 jar 文件规范问题。至于把 spring-boot-loader.jar 这个 jar 包里的文件原封不动的拷贝过来,是为了给应用类加载器(系统加载器)一个程序入口。先加载 JarLauncher 到内容中,再通过自定义类加载器加载自己的应用。

这也回答了之前的问题,为什么不把 spring-boot-loader.jar 这个依赖传递给工程应用,而是在打包的时候,直接打包进工程应用的最顶层。

Last updated

Was this helpful?