Springboot按jar包方式启动,究竟发生了什么
一、背景
在工作的时候,我一直很好奇,当我们以Jar包的方式启动springboot工程时,jvm和springboot的jar工程之间究竟发生了什么样的关联反应,才能走到我们熟悉的业务代码main()函数里(也即下图所示的代码)
java
public static void main(String[] args) {
SpringApplication.run(FirstWebProjectApplication.class, args);
}
为了解答心中的疑惑,我跟踪了一下代码的运行。把得到的答案总结下来,方便和我同样有好奇心的朋友们查阅。
二、核心原理
1.FatJar和ThinJar(胖jar和瘦jar)
顾名思义,胖jar比瘦jar多了一些东西。
SpringBoot的胖jar的结构如下(并不是所有胖jar都是这种结构,这只是SpringBoot的约定俗成):
bash
demo.jar
├── BOOT-INF
├── classes/ # 你写的代码
│ └── lib/ # 所有第三方依赖 jar(几百个)
├── META-INF
│ └── MANIFEST.MF # Main-Class: JarLauncher
└── org/ # SpringBoot 启动器(JarLauncher 所在包)
瘦jar的结构如下:
bash
demo-thin.jar
├── com/ # 仅你的代码
└── META-INF
└── MANIFEST.MF # Main-Class: 你自己的启动类
从以上结构的对比就可以看出来,胖jar多了所有第三方依赖jar,并且包含了SpingBoot的启动器。
进一步,我们来看看实际的springboot工程和普通java工程打包出来的jar包有什么区别。

Springboot工程jar包

最简单的HelloWorld工程jar包
对比以上jar包结构对比,初步猜出jar工程运行的逻辑。
对于瘦jar,jvm先启动,JVM 读取 Jar 包内部的 META-INF/MANIFEST.MF文件,找到Main-Class,这个Main-Class,就是程序的入口。
对于Springboot的胖jar而言,在Main-Class和Start-Class之间增加了一个启动器。springboot的Jar包的启动流程就是,jvm先启动,JVM 读取 Jar 包内部的 META-INF/MANIFEST.MF文件,找到Main-Class,启动Launcher,Launcher启动Start-Class,后面就进入到我们熟悉的代码片段。
2.Springboot的启动器
JarLauncher.java的main()函数如下
java
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
逻辑很简单,new一个JarLauncher的实例,然后调用实例的launch方法。
JarLauncher有一个父类ExecutableArchiveLaucher,这个父类先完成实例化,ExecutableArchiveLaucher的构造函数如下
java
public ExecutableArchiveLauncher() throws Exception {
this(Archive.create(Launcher.class));
}
protected ExecutableArchiveLauncher(Archive archive) throws Exception {
this.archive = archive;
this.classPathIndex = getClassPathIndex(this.archive);
}
首先调用函数Archive.create(Launcher.class),得到Archive实例,赋值给JarLauncher实例的archive变量;
再调用getClassPathIndex(this.archive),将得到的ClassPathIndexFile的实例赋值给JarLauncher实例的classPathIndex变量;
最后调用JarLauncher实例的launch方法。
2.1 JarLauncher的this.archive成员变量
它实际上是JarFileArchive的实例,该实例里有一个成员变量JarFile,就是对springboot的jar包。
2.2 JarLauncher的classPathIndex成员变量
对于采用Jar包启动来说,classPathIndex为null。因为ClassPathIndex文件只在某些特殊场景(如-classpath模式)下才会生成,普通fat jar启动时没有这个文件。
2.3 JarLauncher()的launch()方法
BOOT-INF/classes 和 BOOT-INF/lib 的 URL] IsExploded -->|否| D[配置 JVM 的 URL 协议处理器
注册嵌套 Jar 协议] D --> E E --> F["ClassLoader parent = getClass().getClassLoader();"
获取当前类的 ClassLoader 作为父加载器] F --> G[创建 LaunchedClassLoader
将上述 URL 作为类路径] G --> H[从 MANIFEST.MF 读取 Start-Class] H --> I["Thread.currentThread().setContextClassLoader(classLoader)"
设置线程上下文类加载器] I --> J[反射调用 Start-Class 的 main 方法]
2.4 为什么springboot工程的胖jar需要启动器
简单说,就是springboot工程的胖jar中这个jar包套jar包的结构,用默认的类加载器加载不了,所以要自定义一个LaunchedClassLoader去加载。Spring Boot 的胖 Jar 通过自定义类加载器 LaunchedClassLoader 打破了双亲委派机制,让嵌套在 BOOT-INF/lib 下的依赖 jar 能被正确加载。理解了这个过程,面试时被问到"Spring Boot 启动原理"就不再是死记硬背了。