SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析

Spring Boot 打包之 spring-boot-maven-plugin

Spring Boot 为我们使用、构建和运行 Spring 项目带来了极大的方便,Spring Boot 可以通过 Gradle 或者 Maven 插件将项目构建成可执行的 Jar 包,使得我们写的 Web 项目也可以直接通过 java -jar xxx.jar 方式直接启动,下面我们根据源码来看看,Spring Boot 是如何将代码及依赖的 Jar 包通过插件构建到一个完整的 Jar 包里。下面我们通过 Maven 和 Spring Initializr 创建一个初始 Spring Boot Web 项目,以此项目来进行源码打包流程分析。

Maven 配置

本次示例的环境为 Spring Boot 2.3.9.RELEASE 和 Maven 3.6.3版本,初始化的项目结构如下:

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第1张图片

根据 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。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第2张图片

可以看到最终除了一些编译的文件/文件夹之外,还有两个文件,一个是大小约为 16M 的 .jar 文件,这个就是 Spring Boot 的可执行 Jar 包了(后文简称 fatjar),另一个是以 .original 结尾的只有几kb的文件,这个其实是 maven 生命周期执行时打出的 Jar 包(后文简称 源 jar)。fatjar 其实是 spring-boot-maven-plugin 插件将源 jar 和项目依赖的第三方 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 文件

MANIFEST.MF

源 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 主要有以下差异:

  1. 源 jar 中主项目的 .class 文件被移至 fatjar 的 BOOT-INF/classes 文件夹下。
  2. 新增 BOOT-INF/lib 文件夹,里面存放三方 jar 文件。
  3. 新增 BOOT-INF/classpath.idx,用来记录 classpath 的加载顺序。
  4. 新增 org/springframework/boot/loader 文件夹,这是 spring-boot-loader 编译后的 .class 文件。
  5. 清单文件 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 指令,更加方便我们调试源码。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第3张图片

示例项目结构如下图:

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第4张图片

Goal

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 的源代码结构:

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第5张图片

Mojo

根据 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 的时候回调执行程序想要的操作。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第6张图片

接下来使用 Idea 查看 Mojo 类的实现关系:

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第7张图片

Repackage Goal

执行入口

从上图中的类命名可以推测出 RepackageMojo 就是 repackage goal 的执行入口了。接下来分析 RepackageMojo 类的继承关系和源码。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第8张图片

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第9张图片

上图源码我们可以看到 RepackageMojo 类上使用了 @Mojo 注解,并且指定 name = "repackage",实现了 Mojo 接口,现在我们可以肯定这个类就是 repackage goal 的执行入口了。

开始打包

现在看看 RepackageMojoexecute 方法。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第10张图片

execute 方法中做了一次 pom 判断和 skip 判断后,直接执行了 RepackageMojo 类的 repackage 方法。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第11张图片

上图代码可以看出 RepackageMojo 类的 repackage 方法中主要包含以下几个步骤:

  1. 获取源 jar 的 Artifact 对象。
  2. 创建最终的可执行 jar 文件(仅创建文件,未写入东西)。
  3. 通过 Artifact 构建 Repackage 对象。
  4. 获取项目所依赖的有序的 Libraries
  5. 获取默认的 LaunchScript 启动脚本。
  6. 通过目标 jar ,LibrariesLaunchScript 执行 Repackagerepackage 方法重新打包。
  7. 更新可执行 jar 的 Artifact 信息。

我们继续查看核心的重新打包方法即 Repackage 类的 repackage 方法都做了些什么,这个类已经不属于 spring-boot-maven-plugin 项目的代码了,而是调用到了 spring-boot-loader-tools 模块中的类。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第12张图片

上图代码可以看出 Repackage 类的 repackage 方法中主要包含以下几个步骤:

  1. 获取项目的布局 Layout, 它决定最终打包出来文件的结构。
  2. 校验是否已经打包过。
  3. 备份源 jar 文件,且重命名为 .original 后缀。
  4. 将源 jar 文件删除,但保存路径。
  5. 构建源 jar 的 JarFile 对象。
  6. 通过源 jar, 目标 jar, 依赖包, 启动脚本 调用重构的 repackage 方法构建新的 jar 包。
  7. 如果不需要备份源 jar, 且源 jar 已经备份,则删除源 jar。

文件布局

Repackage 类的 repackage方法中有两个比较重要的步骤,第一个是通过父类 PackagergetLayout 方法获取项目布局的 Layout,这个是决定了我们最终 jar 包目录结构的,我们先看看这块是如何实现的。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第13张图片

由上图可知,该方法会去拿 LayoutFactory 工厂对象,而且首先会去寻找 spring.factories 中已声明的 LayoutFactory,如果没有就返回 DefaultLayoutFactory

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第14张图片

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第15张图片

DefaultLayoutFactory 调用 Layouts#forFile 方法,根据目标文件的后缀名,来决定使用哪种 Layout 实现。我们项目中是可执行 jar,后缀名为 .jar ,则会实例出一个 Layouts#Jar 对象。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第16张图片

看到上图一些返回值大概就明白了文章前半部分分析的 fatjar 中 BOOT-INF 文件夹结构原来是根据这个 Layouts#Jar 布局来的。

写入文件

下面我们看看第二个关键步骤,调用重构的 repackage 方法。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第17张图片

该方法中创建目标 jar 的 JarWriter 对象,然后调用了 write 方法对 jar 进行写入操作。

SpringBoot打包之spring-boot-maven-plugin插件编译可执行Jar包原理分析_第18张图片

小结

在上面方法中,可以看出与文章前面分析的 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 是如何直接启动。

你可能感兴趣的:(Spring,Boot,Maven,架构之路,springboot打包,springboot编译,springbootmaven,springboot,jar包,可执行jar包)