最后更新于
这有帮助吗?
最后更新于
这有帮助吗?
前面在 WebMvc 的部分咱有解析过,打war包运行,使用外部Servlet容器启动 SpringBoot 应用时,需要一个 ServletInitializer
来引导启动 SpringBoot 应用。那在使用jar包启动时,咱只是知道会走主启动类的 main 方法,但那是在开发时直接指定走主启动类的 main 方法,在jar包启动时是另一种方式。咱这最后一篇就来看看jar包启动 SpringBoot 应用的原理。
翻开打好的jar包,会发现3个文件夹:
BOOT-INF
:存放自己编写并编译好的 .class 文件和静态资源文件、配置文件等
META-INF
:有一个 MANIFEST.MF
的文件
org
:spring-boot-loader
的一些 .class 文件
其中,org.springframework.boot.loader
里开始能找到 .class 文件了。
翻看 META-INF
下面的 MANIFEST.MF
文件,发现里面的内容如下:
这个文件中有两个值得关注的:
在 Start-Class
中注明了 SpringBoot 的主启动类
在 Main-Class
中注明了一个类: JarLauncher
如果能靠 SpringBoot 的主启动类完成应用的启动,那为什么还要标注下面的那个 JarLauncher
呢?
发现有个 main 方法!而且上面定义了两个常量,恰好就是在jar包中 BOOT-INF
里面的两个部分:自己的源码,和第三方jar包。
错误: 找不到或无法加载主类 **com.example.demo.DemoApplication**
发现启动失败,根本就找不到这个类。
能正常启动,打印 Banner 等。
用正常的指令启动时,java 指令没有指定 classpath,而当前 SpringBoot 应用依赖的jar包均放在 BOOT-INF/lib
下,这部分无法被识别。
在可执行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包中。
可执行jar包中还有一个规范:jar包中原则上不允许嵌套jar包。
传统的打jar包的方式是将所有依赖的jar包都复制到一个新的jar包中。这样会出现一个致命问题:
如果两个不同的jar包中有一个全限定类名相同的文件,会出现覆盖现象。
SpringBoot 使用自定义的 ClassLoader
,可以解决这个问题,具体的部分要剖析源码才能看到实现机制。
只有这一句,而且从上面的源码中可以发现,调用的空参数构造方法没有任何实际作用,也没有调父类的构造方法。
那一切的功能都在 launch
方法中。launch
方法不在 JarLauncher
里,在父类的 Launcher
内有定义:
从文档注释可以发现,这个方法必须被 main 方法调用,这跟上面的 JarLauncher
中 main 方法直接调用一致。
先设置当前系统的一个变量 **java.protocol.handler.pkgs**
,而这个变量的作用,是设置 URLStreamHandler
实现类的包路径。
之后要重置缓存,目的是清除之前启动的残留(文档注释已标明)。
它要来创建 ClassLoader
,而创建之前先调了 getClassPathArchives
方法来取一些 Archive
对象。
从最后看起,isNestedArchive
方法在调用时要传入 Archive.Entry
,而这个参数的来源尚不明确,先搁置一边。
往前看,有一个 this.archive
,而这个 archive
的成员属性是在这个类创建时被调用的。
Archive
对象最终创建在下面的 createArchive
方法。
到 File root = new File
之前的部分,这段代码都是在找当前类的所在jar包的绝对路径
之后下面把这个文件创建出来,并以此创建一个 JarFileArchive
对象。
而这个 JarFileArchive
是 Archive
的子类,这个 Archive
就可以被 Launcher
启动。文档注释和类定义:
恰巧从 Archive
中得到一个意外收获:Archive
里的 Archive.Entry
可以被迭代!
跟前面的那个方法引用刚好能对应上了。
isNestedArchive
方法传入的参数就是 archive
对象中的那一组 Entry
对象(一个 Entry
相当于一个 "File"
)。
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**
下面的所有文件。
这个后置处理的方法是空的,且没有子类重写,说明默认就是拿 **BOOT-INF/classes**
与 **BOOT-INF/lib**
下面的文件了。
上面部分的源码很容易可以看出是将每个 Archive
的绝对路径保存到一个 List
中,之后调用下面的 createClassLoader
方法。
下面直接创建了一个 LaunchedURLClassLoader
,传入的 ClassLoader
很明显是默认的,也就是 AppClassLoader
。
很简单,直接调用父类的构造方法(指定 ClassLoader
是双亲委托机制)
而 LaunchedURLClassLoader
的父类是: java.net.URLClassLoader
,是jdk内部的 ClassLoader
,不再深入描述。
调用 launch
之前会先调用 getMainClass
方法获取主启动类。
它要从 Launcher
类的成员 archive
中获取 Manifest
文件,这个文件就是之前在 META-INF
下面的 MANIFEST.MF
文件。
之后从这个文件中取出 Start-Class
的值,这个值就是主启动类的全限定类名。
之后调用 launch
方法:
先设置当前线程的上下文类加载器为新的类加载器,也就是 LaunchedURLClassLoader
(默认为 AppClassLoader
)。
之后要开始创建 main 方法的运行器,并运行。
简单的创建了 MainMethodRunner
的对象,之后上面会调用 run 方法。
MainMethodRunner
的结构:
核心是 run 方法。
先拿到当前线程的上下文类加载器,就是 LaunchedURLClassLoader
。
之后用这个 ClassLoader
加载主启动类,之后运行 main 方法。
这也解释了为什么 SpringBoot
应用在开发期间只需要写 main 方法,引导启动即可。