Spring Boot 为我们使用、构建和运行 Spring 项目带来了极大的方便,Spring Boot 可以通过 Gradle 或者 Maven 插件将项目构建成可执行的 Jar 包,使得我们写的 Web 项目也可以直接通过 java -jar xxx.jar
方式直接启动,下面我们根据源码来看看,Spring Boot 是如何将代码及依赖的 Jar 包通过插件构建到一个完整的 Jar 包里。下面我们通过 Maven 和 Spring Initializr 创建一个初始 Spring Boot Web 项目,以此项目来进行源码和打包流程分析。
本次示例的环境为 Spring Boot 2.3.9.RELEASE 和 Maven 3.6.3版本,初始化的项目结构如下:
根据 Spring 官方文档和 pom.xml 文件可以发现,Spring Boot 在 Maven 是通过引入 spring-boot-maven-plugin 插件来构建可执行 Jar 包的。
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
我们可以找到 spring-boot-maven-plugin 官方文档,其实 spring-boot-maven-plugin 的真实配置是下面这样的,配置了一个名为 repackage 的 goal,而我们示例中的项目使用了 spring-boot-starter-parent,它就默认帮我们省去了这个配置。
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
首先可以看看 Sping Boot 的可执行 Jar 和原先 Maven 打出的 Jar 包的结构差异,我们执行 mvn clean package 命令构建一个可执行 Jar。
可以看到最终除了一些编译的文件/文件夹之外,还有两个文件,一个是大小约为 16M 的 .jar
文件,这个就是 Spring Boot 的可执行 Jar 包了(后文简称 fatjar),另一个是以 .original
结尾的只有几kb的文件,这个其实是 maven 生命周期执行时打出的 Jar 包(后文简称 源 jar)。fatjar 其实是 spring-boot-maven-plugin 插件将源 jar 和项目依赖的第三方 Jar 全部打到一个 Jar 包中,并修改或新增一些其他的配置说明文件。我们解压这两个 Jar 看看文件结构差异。
源 Jar 结构:
├── com
│ └── wxdfun
│ └── packagedemo
│ └── PackageDemoApplication.class
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.wxdfun
│ └── package-demo
│ ├── pom.properties
│ └── pom.xml
└── application.properties
fatjar 结构:
├── BOOT-INF
│ ├── classes // 主项目中的 .class 文件
│ ├── com
│ └── wxdfun
│ └── packagedemo
│ └── PackageDemoApplication.class
│ └── application.properties
│ ├── lib
│ ├── spring-boot-starter-2.3.9.RELEASE.jar
│ ├── spring-boot-starter-tomcat-2.3.9.RELEASE.jar
│ ├── spring-boot-starter-web-2.3.9.RELEASE.jar
│ └── ... // 其他三方 jar
│ └── classpath.idx // 记录 classpath 的加载顺序
├── META-INF
│ ├── MANIFEST.MF // jar 清单文件,程序主入口
│ └── maven
│ └── com.wxdfun
│ └── package-demo
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader // spring-boot-loader 的 .class 文件
├── ClassPathIndexFile.class
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
└── ... // 其他 spring-boot-loader 文件
源 Jar 清单文件 MANIFEST.MF
:
Manifest-Version: 1.0
Implementation-Title: package-demo
Implementation-Version: 0.0.1-SNAPSHOT
Build-Jdk-Spec: 1.8
Created-By: Maven Jar Plugin 3.2.0
fatjar 清单文件 MANIFEST.MF
:
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: package-demo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.wxdfun.packagedemo.PackageDemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.9.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
从上面的文件结构和 jar 清单内容来看,Spring Boot 打包后的 fatjar 对比 源 jar 主要有以下差异:
.class
文件被移至 fatjar 的 BOOT-INF/classes
文件夹下。BOOT-INF/lib
文件夹,里面存放三方 jar 文件。BOOT-INF/classpath.idx
,用来记录 classpath 的加载顺序。org/springframework/boot/loader
文件夹,这是 spring-boot-loader 编译后的 .class
文件。MANIFEST.MF
中新增以下属性:
Spring-Boot-Classpath-Index
: 记录 classpath.idx
文件的地址。Start-Class
: 指定 Spring Boot 的启动类。Spring-Boot-Classes
: 记录主项目的 .class
文件存放路径。Spring-Boot-Lib
: 记录三方 jar 文件存放路径。Spring-Boot-Version
: 记录 Spring Boot 版本信息Main-Class
: 指定 jar 程序的入口类(可执行 jar 为 org.springframework.boot.loader.JarLauncher
类)。Spring Boot 通过 spring-boot-maven-plugin 将源 jar 重新打包成 fatjar,下面我们将对 spring-boot-maven-plugin 源码进行分析,了解下是经过了哪些步骤。spring-boot-maven-plugin 是 Maven 中的一个构建插件,对 Maven 插件不了解的童鞋,可以去 Maven Plugins 官方文档 中学习相关知识。
我们可以从 Spring Boot 在 Github 上的 托管仓库 获得源码信息,也可以在任意项目中加入 spring-boot-maven-plugin 的依赖来下载它的 source 文件获取源码。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.3.9.RELEASEversion>
<scope>providedscope>
dependency>
我建议两者都做,获取 Spring Boot 仓库源码后,可以更清晰的阅读源码,也可以阅读其他相关模块的源码。示例项目中添加依赖后,可以通过 debug 模式执行 mvn package
指令,更加方便我们调试源码。
示例项目结构如下图:
spring-boot-maven-plugin 官方文档 介绍的有五种 goal,分别如下:
spring-boot:build-image
: 将程序使用 buildpack 打包进容器镜像中。spring-boot:build-info
: 根据当前 MavenProject
的内容生成一个 build-info.properties
文件spring-boot:help
: 显示帮助信息。调用mvn spring-boot:help -Ddetail=true -Dgoal=
以显示参数详细信息。spring-boot:repackage
: 默认的 goal,将普通 mvn package
打包成的 jar 重新打包成包含所有程序依赖项的可执行 jar/war 文件,并保留 mvn package
打包的 jar 为 .original
后缀spring-boot:run
: 运行 Spring Boot 应用。spring-boot:start
: 通常用于集成测试方案中,在 mvn integration-test
阶段管理 Spring Boot 应用的生命周期。spring-boot:stop
: 停止已通过 start
目标启动的应用程序。通常在 integration-test
完成后调用。文章只对 repackage 进行分析,我们先直接看下 spring-boot-maven-plugin 的源代码结构:
根据 Maven Plugins 官方文档 介绍,Maven 插件中的 goal 即为一个 Mojo。相当于要定义一个 goal,就需要有一个类去实现 Mojo。官方解释,Mojo 就是Maven plain Old Java Object,每一个 Mojo 就是 Maven 中的一个执行目标(executable goal),而插件则是对单个或多个相关的 Mojo 做统一分发。简单说,你写 Maven 插件,Mojo就是入口。我们先查看 Mojo
类的源码,可以看出 Mojo
类的核心是 execute
方法,这个方法会在 Maven 执行构建 goal 的时候回调执行程序想要的操作。
接下来使用 Idea 查看 Mojo
类的实现关系:
从上图中的类命名可以推测出 RepackageMojo
就是 repackage goal 的执行入口了。接下来分析 RepackageMojo
类的继承关系和源码。
上图源码我们可以看到 RepackageMojo
类上使用了 @Mojo
注解,并且指定 name = "repackage"
,实现了 Mojo
接口,现在我们可以肯定这个类就是 repackage goal 的执行入口了。
现在看看 RepackageMojo
的 execute
方法。
execute
方法中做了一次 pom 判断和 skip 判断后,直接执行了 RepackageMojo
类的 repackage
方法。
上图代码可以看出 RepackageMojo
类的 repackage
方法中主要包含以下几个步骤:
Artifact
对象。Repackage
对象。Libraries
。LaunchScript
启动脚本。Libraries
和 LaunchScript
执行 Repackage
的 repackage
方法重新打包。Artifact
信息。我们继续查看核心的重新打包方法即 Repackage
类的 repackage
方法都做了些什么,这个类已经不属于 spring-boot-maven-plugin 项目的代码了,而是调用到了 spring-boot-loader-tools 模块中的类。
上图代码可以看出 Repackage
类的 repackage
方法中主要包含以下几个步骤:
Layout
, 它决定最终打包出来文件的结构。.original
后缀。JarFile
对象。repackage
方法构建新的 jar 包。Repackage
类的 repackage
方法中有两个比较重要的步骤,第一个是通过父类 Packager
的 getLayout
方法获取项目布局的 Layout
,这个是决定了我们最终 jar 包目录结构的,我们先看看这块是如何实现的。
由上图可知,该方法会去拿 LayoutFactory
工厂对象,而且首先会去寻找 spring.factories
中已声明的 LayoutFactory
,如果没有就返回 DefaultLayoutFactory
。
DefaultLayoutFactory
调用 Layouts#forFile
方法,根据目标文件的后缀名,来决定使用哪种 Layout
实现。我们项目中是可执行 jar,后缀名为 .jar
,则会实例出一个 Layouts#Jar
对象。
看到上图一些返回值大概就明白了文章前半部分分析的 fatjar 中 BOOT-INF 文件夹结构原来是根据这个 Layouts#Jar
布局来的。
下面我们看看第二个关键步骤,调用重构的 repackage
方法。
该方法中创建目标 jar 的 JarWriter
对象,然后调用了 write
方法对 jar 进行写入操作。
在上面方法中,可以看出与文章前面分析的 fatjar 的文件结构相关的都有写入,至此 spring-boot-maven-plugin 的repackage 也到此结束。其中 META-INF/loader/spring-boot-loader.jar
这个文件是直接打包在 spring-boot-maven-plugin 这个包中的 jar 文件,它是 Spring Boot 的启动器,Spring Boot 可以直接用 jar 或者文件夹的方式启动也是通过 spring-boot-loader 这个模块实现的,后面我们再讲讲 Spring Boot 是如何直接启动。