Spring Boot 启动原理解析

本贴最后更新于 1597 天前,其中的信息可能已经时异事殊

与传统的 web 应用服务部署不同,Spring Boot 提供了 java -jar 这种方式的一键部署,不再需要单独部署 tomcat 实例,使得部署变得相当简单。这背后究竟是如何实现的呢?要想分析这个问题,我们需要先了解 Spring Boot 打包后的 jar 文件,究竟是什么样子的。

1. 目录结构分析

我们在 IDEA 中新建一个 Spring Boot 工程,叫 spring-boot-demo,打包之后,得到一个 jar 文件:spring-boot-demo-0.0.1-SNAPSHOT.jar,我们用 unzip 命令解压该 jar 包后,能够看到对应的目录结构,如下图所示:

$ tree -L 3
.
├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   ├── cn
│   │   ├── static
│   │   └── templates
│   └── lib
│       ├── spring-core-5.2.2.RELEASE.jar
│       ├── spring-webmvc-5.2.2.RELEASE.jar
│       ├── ...// 这里略了大量jar包
├── META-INF
│   └── MANIFEST.MF
└── org
    └── springframework
        └── boot 

我们注意到,工程中的源代码部分编译完成后会进入 BOOT-INF/classes 文件夹下,工程的依赖会进入 BOOT-INF/lib 目录下。除此之外还有一个 META-INF 与 org/springframework/boot/..的文件夹。看到这个,不禁就有疑问,这两个文件夹是干嘛用的?

我们先来看 META-INF 目录,这个目录下就只有一个 MANIFEST.MF 文件,我们看下里面的内容:

$ cat META-INF/MANIFEST.MF 
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: cn.xiajl.springbootdemo.SpringBootDemoApplication
Spring-Boot-Version: 2.2.2.RELEASE
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/ 

我们可以查询下中 oracle 对该文件的定义,见参考资料 1。我们会发现,只有 Manifest-Version、Main-Class 是 oracle 定义的,Manifest-Version 表示 jar 包的版本号,Main-Class 表示 jar 启动时的启动类。其它的 Start-Class、Spring-Boot-Version、Spring-Boot-Classes、Spring-Boot-Lib 都不在 MANIFEST.MF 的规范里,换句话说,这是 SpringBoot 自己定义的。

这里有个问题,既然 Main-Class 是启动类,那么 Main-Class 为什么是 org.springframework.boot.loader.JarLauncher,而不是 cn.xiajl.springbootdemo.SpringBootDemoApplication?

我们可以尝试把 Main-Class 改成工程中的 SpringBootDemoApplication 类试试,打包回去,尝试看看能不能启动:

$ vi META-INF/MANIFEST.MF 
$ cat META-INF/MANIFEST.MF 
Manifest-Version: 1.0
Start-Class: org.springframework.boot.loader.JarLauncher
Main-Class: cn.xiajl.springbootdemo.SpringBootDemoApplication
Spring-Boot-Version: 2.2.2.RELEASE
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
$ jar -cvf0M rarpack.jar *
...//略去输出
$ java -jar rarpack.jar 
错误: 找不到或无法加载主类 cn.xiajl.springbootdemo.SpringBootDemoApplication
原因: java.lang.ClassNotFoundException: cn.xiajl.springbootdemo.SpringBootDemoApplication 

很明显启动不了。那看到这里,很自然的就能想到,这个 JarLauncher 是不是要帮我们解决类路径的问题?jar 启动的时候,按照 oracle 的规范似乎要手工指定 classpath,并且也没有办法定位到 jar 包中的 BOOT-INF/BOOT-Classes 路径,只会加载 jar 包下直接解决的 class 文件。

2. JarLauncher 分析

当思考到这里的时候,我们像往常一样,在 Idea 里面搜 JarLauncher 这个类,试图进去看看这个类的源码,你会发现找不到。为什么呢?因为这个是插件导进去的,对于 gradle 编译的工程,是在 bootJar 阶段,将 spring-boot-loader 中类直接解压拷贝到 jar 包中的。那我们怎么才能看到源码呢?直接在依赖里加上 spring-boot-loader 即可:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // 仅用于研究spring-boot-loader,实际工程中不需要该依赖
    implementation group: 'org.springframework.boot', name: 'spring-boot-loader', version: '2.2.2.RELEASE'
} 

打开 JarLancher 的源码,我们会看一下类的说明:

Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.

文档很明确地说明了,它是来帮我们加载/BOOT-INF/lib 下的 jar 包与/BOOT-INF/classes 下的 classes 文件。
那它到底是怎么帮我们加载的呢?

我们跟进源码,JarLancher 中存在如下 main 方法:

public static void main(String[] args) throws Exception {
    new JarLauncher().launch(args);
} 

我们来看看 launch 是怎么实现的:

/**
 * Launch the application. This method is the initial entry point that should be
 * called by a subclass {@code public static void main(String[] args)} method.
 * @param args the incoming arguments
 * @throws Exception if the application fails to launch
 */
protected void launch(String[] args) throws Exception {
    JarFile.registerUrlProtocolHandler();
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    launch(args, getMainClass(), classLoader);
}

/**
 * Create a classloader for the specified URLs.
 * @param urls the URLs
 * @return the classloader
 * @throws Exception if the classloader cannot be created
 */
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
    return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

/**
 * Launch the application given the archive file and a fully configured classloader.
 * @param args the incoming arguments
 * @param mainClass the main class to run
 * @param classLoader the classloader
 * @throws Exception if the launch fails
 */
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
    Thread.currentThread().setContextClassLoader(classLoader);
    createMainMethodRunner(mainClass, args, classLoader).run();
} 

代码一目了然,Spring Boot 会为我们创建一个自定义类加载器 LaunchedURLClassLoader,并将其设置为线程上下文类加载器。应用执行过程
中,会从 LaunchedURLClassLoader 进行类加载,这个类加载过程会遵循双亲委派机制,对于父类无法加载的类,则由 LaunchedURLClassLoader 进行加载,LaunchedURLClassLoader 加载的路径就是 BOOT-INF/lib 和 BOOT-INF/classes。

3. 为什么需要 BOOT-INF/lib 与 BOOT-INF/classes?

执行逻辑我们搞清楚了,我们还需要了解一下为什么 Spring Boot 不按照传统的方式,即像 Spring Boot Loader 中的类一样,将所有的依赖解压放到 jar 中呢?是出于什么考虑?
这里其实是有很多工程上的考量,我这边里总结一下:

  1. 不同组件之前也有可能存在重名的情况,同样的组件不同版本之间也是不能兼容的。在解压时,无法预测知道重名的那些文件,哪个组件的哪个版本会被保留下来。
  2. SPI 机制规范产生的配置文件很多时候是同名的,比如 spring.factories(如下图所示),这种配置文件是不能覆盖的,必须保留多份。

image.png

因此,Spring Boot 创新性的采用自定义类加载路径的方式来进行。生成的这种带依赖的 jar 也有一个专有的名词,叫 FatJar/UberJar,如下图所示(来源:参考资料 2):

image.png

参考资料

  1. https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html
  2. https://dzone.com/articles/the-skinny-on-fat-thin-hollow-and-uber
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    941 引用 • 1458 回帖 • 150 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...