《SpringBoot 3:入门与应用实战》第 14 章 打包与部署 Spring Boot 应用打包 阅读笔记 41
通过前面几个部分的学习,已经可以熟练地完成应用开发和场景整合,但是只在开发主机上运行是无法正常对外提供长期稳定的服务的。接下来要做的是将开发好的应用打包并部署到服务器中,让用户访问和使用服务器中的 Web 应用。Spring Boot 的强大特性之一是利用嵌入式 Web 容器直接运行工程,也可以使用外置的 Servlet 容器运行,还可以将工程打包成 Docker 镜像部署到 Docker 容器中,甚至是云平台的容器环境。下面从打包开始依次讲解。
14.1 Spring Boot 应用打包
无论是使用原生 Servlet 开发的 Web 应用,还是基于 Spring Framework + WebMvc 开发的 Web 应用,最终都需要部署到一个独立运行的Servlet 容器(如 Tomcat)中才可以正常运行,而基于 Spring Boot 的应用借助嵌入式 Web 容器可以做到独立运行,也正是因为不需要借助外置的 Servlet 容器,使得 Spring Boot 的单体工程完全可以打包成 jar 包运行,使用 jar 包部署和运行相对灵活,且可以相对简单地完成启动和扩容等工作。
14.1.1 制作简易工程
首先制作一个方便演示打包和部署的简易工程 springboot-12-package,这里面只需要导入 WebMvc 的场景启动器 spring-boot-starter-web。
再新建一个 Controller 即可。有了这些代码后,就拥有了演示打包和部署的基本条件。



14.1.2 使用 Maven 打包工程
默认情况下在 pom.xml 中声明的打包方式为 jar,使用 Maven 打包只需要执行 mvn package 命令,可以在 springboot-package-a 工程的根目录下使用命令行工具执行 mvn package 命令,也可以直接使用 IDEA 的 Maven 面板找到 springboot-package-a 工程的生命周期,并执行 package 命令。注意,笔者在打包时禁用了 test 环节,这是因为正常情况下 Maven 在执行 package 生命周期命令时,会将工程中的所有单元测试都执行一次,且不论每次执行全部单元测试会耗费多长时间。如果单元测试中有对数据库的操作或者对外部接口的请求等可能造成影响的内容,那么每次打包都有可能产生意外的情况。为了避免上述问题,在开发和打包阶段建议读者禁用 test 环节。

打包完成后,在工程目录的 target 下可以找到一个 jar 包,这个 jar 包的命名规则为 "{artifactId}-{version}.jar"。

14.1.3 运行工程与打包插件
运行 jar 包的方式非常简单,在当前目录下唤起命令行窗口,使用 java -jar xxx.jar 命令即可启动 jar 包,从控制台输出的内容中可以发现与使用IDE 工具运行工程的效果基本一致。
运行工程本身非常简单,本节要着重讲解的是另一点。读者是否注意到在导入依赖时 pom.xml 文件中有一个 spring-boot-maven-plugin 插件,这个插件的作用非常重要,只有导入这个插件后打包得到的 jar 文件才可以正常运行,所以使用 Spring Boot 开发项目时一定不要落下这个插件。图是没有导入 spring-boot-maven-plugin 插件和导入插件之后打包运行效果的对比,可以发现当没有导入 spring-boot-maven-plugin 插件时,使用 java -jar 命令运行 jar 包时会提示找不到主清单属性,实际上就是找不到 main 方法;而导入 spring-boot-maven-plugin 插件后该插件会帮把当前工程中依赖的所有jar包都汇总到最终打包好的 jar 包中,顺便也会标记当前主启动类的位置。



1.可执行 jar 包的前置知识
从 Oracle 官网上可以找到有关 jar 文件的规范文档,文档中提到了一个核心目录:META-INF,这个目录中会存放当前 jar 包的一些扩展和配置数据,其中有一个核心配置文件叫 MANIFEST.MF,它以 properties 的配置格式保存了 jar 包的部分核心元信息。MANIFEST.MF 文件中主要包含表所示的核心配置项内容,由于配置项比较多,本节只挑选几个下面会提到的配置项,关于全部的属性信息读者可以参照规范文档自行了解。
| 属性类别 | 核心配置项 (Key) | 作用与说明 |
|---|---|---|
| 一般属性 | Manifest-Version |
定义清单文件本身的版本号,通常为 1.0。 |
Created-By |
声明生成该 JAR 文件的工具或 JDK 版本信息。 | |
| 应用程序属性 | Main-Class |
指定 JAR 文件的可执行入口类,配置后可通过 java -jar 直接运行。 |
Class-Path |
声明运行时依赖的 JAR 文件或目录的相对路径,多个路径间以空格分隔。 | |
| 包扩展与版本属性 | Extension-Name |
定义 JAR 文件的扩展标识。 |
Implementation-Title/Version/Vendor |
声明扩展实现的标题、版本号以及开发组织。 | |
Specification-Title/Version/Vendor |
声明扩展规范的标题、版本号以及维护该规范的组织。 | |
Sealed |
定义包是否"密封"(值为 true 或 false),密封包要求所有类必须来自同一个 JAR 文件。 |
|
| 签名相关属性 | Name |
指定被签名的具体条目(如某个 .class 文件)的路径。 |
Digest-Algorithms |
声明用于验证文件完整性的摘要算法(如 SHA、MD5)。 |
|
SHA-Digest / MD5-Digest |
存储文件内容经过 Base64 编码后的摘要值,用于防篡改校验。 | |
| 其他扩展属性 | Multi-Release |
JDK 9+ 引入,用于支持在同一个 JAR 中存放多版本的类文件。 |
| 自定义属性 | 开发者可根据特定业务需求,自行添加的键值对信息。 |
请读者重点关注一个配置项:Main-Class。它需要指定一个可以在 jar 包的顶层结构中可以直接找到的、带有 main 方法的启动类的全限定名,所谓的顶层结构指的是 jar 包中可以直接在目录中找到的、不需要再解压 / 探寻 jar 包内部。注意这里又出现了一个新的概念:jar 包中可能会嵌套 jar 包,这种类型的 jar 包被称为 "Fat Jar",这种类型的 jar 包可以解决第三方库不在 classpath 下的加载失败问题,Spring Boot 生成的可执行 jar 包本身就是一种 Fat Jar。
2.两次打包的 jar 包对比
使用压缩软件分别打开两次打包的 jar 文件,第一眼的不同之处是文件夹结构,图中左侧是没有使用 spring-boot-maven-plugin 插件打包之后的jar 文件,可以看到内部有编写的 Java 代码编译后的包以及放置在 src/main/resources 下的全局配置文件,除此之外它包含一个META-INF目录,也就是上面刚提到的那个包含元信息的文件夹;
右侧是使用 spring-boot-maven-plugin 插件打包后的 jar 文件,不能直接看到内部有编写的所有代码,如果手动浏览,可以在 BOOT-INF/classes 下找到编写的所有代码,而在 BOOT-INF/lib 下有当前工程中依赖所有的 jar 包。



再到 META-INF 目录中对比两个 MANIFEST.MF 文件,可以发现右侧可以正常执行的 jar 包中的 MANIFEST.MF 文件中包含两个信息,分别是 Main-Class 和 Start-Class,虽然并不清楚 Start-Class 是什么,但从值上已经可以看出它就是编写的 Spring Boot 主启动类,而引导触发主启动类的真正启动类是一个叫 JarLauncher 的类,它被标注为 Main-Class;反观左侧无法正常运行的 jar 包中的 MANIFEST.MF 文件,它压根儿就没有 Main-Class 信息,所以这个 jar 包就无法使用 java -jar 命令执行。

3.Fat Jar 可以正确执行的原理
梳理清楚两个 jar 文件的区别后,下面可以简单总结一下使用 spring-boot-maven-plugin 插件打包的可执行 jar 文件的执行原理。
(1) 由于 MANIFEST.MF 文件包含 Main-Class 信息,因此jar文件可以通过使用 java -jar 命令执行。
(2) 由于 BOOT-INF 下保存了工程中的所有代码和依赖的 jar 包,具备支撑运行的条件。
(3) 引导可执行 jar 文件执行的 JarLauncher 可以找到 MANIFEST.MF 文件中的 Start-Class 信息,从而找到 Spring Boot 的主启动类。
(4) 执行主启动类的 main 方法,Spring Boot 应用被成功启动。