Spring Boot JarLauncher

前面在 WebMvc 的部分咱有解析过,打war包运行,使用外部Servlet容器启动 SpringBoot 应用时,需要一个 ServletInitializer 来引导启动 SpringBoot 应用。那在使用jar包启动时,咱只是知道会走主启动类的 main 方法,但那是在开发时直接指定走主启动类的 main 方法,在jar包启动时是另一种方式。咱这最后一篇就来看看jar包启动 SpringBoot 应用的原理。

翻开打好的jar包,会发现3个文件夹:

  • BOOT-INF:存放自己编写并编译好的 .class 文件和静态资源文件、配置文件等

  • META-INF:有一个 MANIFEST.MF 的文件

  • orgspring-boot-loader 的一些 .class 文件

其中,org.springframework.boot.loader 里开始能找到 .class 文件了。

翻看 META-INF 下面的 MANIFEST.MF 文件,发现里面的内容如下:

Manifest-Version: 1.0
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.9.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class:org.springframework.boot.loader.JarLauncher

这个文件中有两个值得关注的:

  • Start-Class 中注明了 SpringBoot 的主启动类

  • Main-Class 中注明了一个类: JarLauncher

如果能靠 SpringBoot 的主启动类完成应用的启动,那为什么还要标注下面的那个 JarLauncher 呢?

1. JarLauncher是什么东西

发现有个 main 方法!而且上面定义了两个常量,恰好就是在jar包中 BOOT-INF 里面的两个部分:自己的源码,和第三方jar包。

2. 测试直接启动两个带main方法的类

2.1 SpringBootApplication

错误: 找不到或无法加载主类 **com.example.demo.DemoApplication**

发现启动失败,根本就找不到这个类。

2.2 JarLauncher

能正常启动,打印 Banner 等。

3. 【拓展】主启动类无法正常引导启动的原理

用正常的指令启动时,java 指令没有指定 classpath,而当前 SpringBoot 应用依赖的jar包均放在 BOOT-INF/lib 下,这部分无法被识别。

3.1 标准jar包的启动规范

在可执行jar包中,有一个规范:被标记为Main-Class的类必须连同自己的包,直接放在jar包的最外层(没有额外的文件夹包含)。

  • SpringBootApplication 的位置:"BOOT-INF/classes/com.example.demo.DemoApplication.class"

  • JarLauncher 的位置:"org.springframework.boot.loader.JarLauncher"

所以 JarLauncher 能引导成功,而直接运行主启动类却无法成功启动。

这也解释了另外一个现象:

SpringBoot 在打jar包时,没有直接将 spring-boot-loader 包直接依赖到lib目录,而是将这个包下面的所有 .class 文件都复制到要打的jar包中。

3.2 标准jar包的内嵌jar规范

可执行jar包中还有一个规范:jar包中原则上不允许嵌套jar包。

传统的打jar包的方式是将所有依赖的jar包都复制到一个新的jar包中。这样会出现一个致命问题:

如果两个不同的jar包中有一个全限定类名相同的文件,会出现覆盖现象。

SpringBoot 使用自定义的 ClassLoader,可以解决这个问题,具体的部分要剖析源码才能看到实现机制。

4. JarLauncher的main方法都做了什么

只有这一句,而且从上面的源码中可以发现,调用的空参数构造方法没有任何实际作用,也没有调父类的构造方法。

那一切的功能都在 launch 方法中。launch 方法不在 JarLauncher 里,在父类的 Launcher 内有定义:

从文档注释可以发现,这个方法必须被 main 方法调用,这跟上面的 JarLauncher 中 main 方法直接调用一致。

4.1 registerUrlProtocolHandler

先设置当前系统的一个变量 **java.protocol.handler.pkgs**,而这个变量的作用,是设置 URLStreamHandler 实现类的包路径。

之后要重置缓存,目的是清除之前启动的残留(文档注释已标明)。

4.2 createClassLoader

它要来创建 ClassLoader,而创建之前先调了 getClassPathArchives 方法来取一些 Archive 对象。

4.2.1 getClassPathArchives

从最后看起,isNestedArchive 方法在调用时要传入 Archive.Entry,而这个参数的来源尚不明确,先搁置一边。

往前看,有一个 this.archive,而这个 archive 的成员属性是在这个类创建时被调用的。

Archive 对象最终创建在下面的 createArchive 方法。

File root = new File 之前的部分,这段代码都是在找当前类的所在jar包的绝对路径

之后下面把这个文件创建出来,并以此创建一个 JarFileArchive 对象。

而这个 JarFileArchiveArchive 的子类,这个 Archive 就可以被 Launcher 启动。文档注释和类定义:

恰巧从 Archive 中得到一个意外收获:Archive 里的 Archive.Entry 可以被迭代!

跟前面的那个方法引用刚好能对应上了。

isNestedArchive 方法传入的参数就是 archive 对象中的那一组 Entry 对象(一个 Entry 相当于一个 "File")。

4.2.2 getNestedArchives

archive 对象要执行 getNestedArchives 时,会传入一个 EntryFilter,以此来获取一组被嵌套的 Archive

而这个 EntryFilter 的工作机制就是上面的 isNestedArchive 方法,在 JarLauncher 中也有定义:

很明显,看看是不是 BOOT-INF/lib 开头的jar包,如果不是,看看是不是 BOOT-INF/classes 文件夹。

这部分的意义正好跟前面测试主启动类与 JarLauncher 的启动相呼应:

位于 BOOT-INF/classes 的启动类需要后续被扫描到,才能被处理

由此可见,得到的 archives 集合就是 **BOOT-INF/classes****BOOT-INF/lib** 下面的所有文件。

4.2.3 postProcessClassPathArchives

这个后置处理的方法是空的,且没有子类重写,说明默认就是拿 **BOOT-INF/classes****BOOT-INF/lib** 下面的文件了。

4.3 createClassLoader

上面部分的源码很容易可以看出是将每个 Archive 的绝对路径保存到一个 List 中,之后调用下面的 createClassLoader 方法。

下面直接创建了一个 LaunchedURLClassLoader,传入的 ClassLoader 很明显是默认的,也就是 AppClassLoader

4.3.1 构造方法

很简单,直接调用父类的构造方法(指定 ClassLoader 是双亲委托机制)

LaunchedURLClassLoader 的父类是: java.net.URLClassLoader ,是jdk内部的 ClassLoader,不再深入描述。

4.4 launch

调用 launch 之前会先调用 getMainClass 方法获取主启动类。

它要从 Launcher 类的成员 archive 中获取 Manifest 文件,这个文件就是之前在 META-INF 下面的 MANIFEST.MF 文件。

之后从这个文件中取出 Start-Class 的值,这个值就是主启动类的全限定类名。

之后调用 launch 方法:

先设置当前线程的上下文类加载器为新的类加载器,也就是 LaunchedURLClassLoader (默认为 AppClassLoader)。

之后要开始创建 main 方法的运行器,并运行。

4.5 mainMethodRunner.run

简单的创建了 MainMethodRunner 的对象,之后上面会调用 run 方法。

MainMethodRunner 的结构:

核心是 run 方法。

先拿到当前线程的上下文类加载器,就是 LaunchedURLClassLoader

之后用这个 ClassLoader 加载主启动类,之后运行 main 方法。

这也解释了为什么 SpringBoot 应用在开发期间只需要写 main 方法,引导启动即可。

最后更新于

这有帮助吗?