本节目标:用
mvn package把 demo 打成可执行 jar,看懂BOOT-INF/classes、BOOT-INF/lib与JarLauncher的内部结构,用java -jar独立运行,并理解 4.0 移除启动脚本、4.1 用tools取代layertools这两条变更,以及分层 jar 与 Docker 镜像的关系。
适用版本:Spring Boot 4.1.x(Java 21)
mvn package 实测
前两节我们一直用 ./mvnw spring-boot:run 在开发态运行。要发布出去,得把它打包。在项目根目录执行:
./mvnw clean package
clean 先删掉上一次的 target/,package 再重新构建。实测输出(4.1.1,非首次构建,省略了依赖下载行):
[INFO] Scanning for projects...
[INFO] -------------------------< com.example:demo >--------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] --- clean:3.4.1:clean (default-clean) @ demo ---
[INFO] Deleting /Users/me/demo/target
[INFO] --- resources:3.3.1:resources (default-resources) @ demo ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] --- compiler:3.13.0:compile (default-compile) @ demo ---
[INFO] Compiling 2 source files with javac [debug release 21] to target/classes
[INFO] --- resources:3.3.1:testResources (default-testResources) @ demo ---
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ demo ---
[INFO] Compiling 1 source file with javac [debug release 21] to target/test-classes
[INFO] --- surefire:3.5.2:test (default-test) @ demo ---
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jar:3.5.1:jar (default-jar) @ demo ---
[INFO] Building jar: /Users/me/demo/target/demo-0.0.1-SNAPSHOT.jar
[INFO] --- spring-boot:4.1.1:repackage (repackage) @ demo ---
[INFO] Replacing main artifact with repackaged archive
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 38.421 s
两个关键动作:jar 插件先生成一个普通的 jar,随后 spring-boot:repackage 把它替换成可执行 jar(日志里的 Replacing main artifact with repackaged archive 就是这步)。产物在 target/ 下:
target/demo-0.0.1-SNAPSHOT.jar <- 可执行 jar,直接 java -jar 运行
target/demo-0.0.1-SNAPSHOT.jar.original <- repackage 之前的普通 jar
.original 是替换前的原始产物,只包含你自己的 class,不含任何依赖,不能独立运行。看到两个文件不要困惑——带 .original 后缀的才是「原材料」。
可执行 jar 的内部结构
可执行 jar 也叫 fat jar 或 uber jar。用 unzip -l 看它内部:
unzip -l target/demo-0.0.1-SNAPSHOT.jar
Archive: target/demo-0.0.1-SNAPSHOT.jar
Length Date Time Name
--------- ---------- ----- ----
0 09-12-2026 20:00 META-INF/
481 09-12-2026 20:00 META-INF/MANIFEST.MF
0 09-12-2026 20:00 org/
0 09-12-2026 20:00 org/springframework/boot/loader/launch/
855 02-01-1980 00:00 org/springframework/boot/loader/launch/JarLauncher.class
0 09-12-2026 20:00 BOOT-INF/
0 09-12-2026 20:00 BOOT-INF/classes/
1209 09-12-2026 20:00 BOOT-INF/classes/com/example/demo/HelloController.class
738 09-12-2026 20:00 BOOT-INF/classes/com/example/demo/DemoApplication.class
0 09-12-2026 20:00 BOOT-INF/lib/
287122 09-12-2026 20:00 BOOT-INF/lib/spring-boot-4.1.1.jar
...
四个区域各司其职:
| 路径 | 内容 | 作用 |
|---|---|---|
META-INF/ | MANIFEST.MF | 描述 jar 的入口与布局 |
org/springframework/boot/loader/ | JarLauncher 等 | Spring Boot 自带的启动器 |
BOOT-INF/classes/ | 你自己编译出的 class | 应用代码 |
BOOT-INF/lib/ | 所有依赖 jar | 第三方库与 Spring 全家桶 |
注意依赖不是被展开成一个个 class,而是以嵌套 jar 的形式原样放在 BOOT-INF/lib/ 下。这就是为什么不能直接 java -cp app.jar——标准类加载器不认识「jar 里的 jar」。
MANIFEST.MF 里的关键字段
真正让这个 jar「可执行」的,是 META-INF/MANIFEST.MF:
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Version: 4.1.1
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
逐条看:
Main-Class指向JarLauncher,不是你的DemoApplication。JVM 启动的是这个启动器,它负责把BOOT-INF/lib里的嵌套 jar 挂到自定义类加载器上。Start-Class才是你的应用主类。JarLauncher加载完 classpath 后,再反射调用Start-Class的main方法。Spring-Boot-Classes/Spring-Boot-Lib告诉启动器去哪找应用类和依赖。Spring-Boot-Classpath-Index与Spring-Boot-Layers-Index是索引文件,前者加速类路径构建,后者服务于分层 jar(后面讲)。
对比 .original 的 MANIFEST,它只有 Main-Class(甚至是空的)、没有 Start-Class,所以直接 java -jar 那个文件会报「no main manifest attribute」。
java -jar 运行与实测输出
把 jar 拷到任意目录,它都能独立运行——这就是「构建产物即运行环境」:
java -jar target/demo-0.0.1-SNAPSHOT.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.1.1)
2026-09-12T20:04:11.203+08:00 INFO 51207 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication v0.0.1-SNAPSHOT using Java 21.0.12.1 with PID 51207
2026-09-12T20:04:12.118+08:00 INFO 51207 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-09-12T20:04:12.246+08:00 INFO 51207 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.043 seconds (process running for 1.688)
与 3.2 的启动日志结构完全一致。用 curl http://localhost:8080/hello 验证,响应正常。spring-boot:run 与 java -jar 跑的是同一套代码,区别只在于类路径的组织方式。
4.0 已移除嵌入式可执行 jar 启动脚本
在 3.x 里,如果你在插件里配置了 <executable>true</executable>,打出的 jar 会在文件头部附加一段 shell 脚本,使它变成「fully executable jar」。于是你可以像运行普通可执行文件一样运行它:
./demo-0.0.1-SNAPSHOT.jar start # 3.x 的用法,4.x 已移除
Spring Boot 4.0 移除了嵌入式启动脚本。原因主要有二:该特性只对类 Unix 系统有效,且与官方推荐的「高效部署」方式相冲突。官方给出的替代建议是:
- 直接用
java -jar运行(最通用); - 用 systemd 管理进程,把
java -jar写进 unit 文件; - 用容器镜像承载(下一节展开)。
也就是说,4.x 里 .jar 就是纯粹的 jar,不再是「披着 jar 外壳的脚本」。如果你从 3.x 迁移过来,任何依赖 ./app.jar start|stop|status 的脚本都需要改写。
4.1 移除了 layertools jar 模式
分层 jar(layered jar)是 Spring Boot 2.3 引入的能力:把 fat jar 拆成「依赖层」「快照依赖层」「应用层」等,让 Docker 构建时能复用不变层。3.x 里通过 jarmode 操作它:
# 3.x 的用法,4.1 已移除
java -Djarmode=layertools -jar demo-0.0.1-SNAPSHOT.jar extract
Spring Boot 4.1 的发布说明明确写着:已废弃的 layertools jar 模式被移除,请改用 tools,后者提供相同甚至更多的能力。对应命令变成:
java -Djarmode=tools -jar demo-0.0.1-SNAPSHOT.jar extract --layers --destination extracted
这条变更有一个可以直接验证的证据:可执行 jar 的 BOOT-INF/lib/ 里出现了 spring-boot-jarmode-tools-4.1.1.jar,而不再有 layertools 相关的类。tools 模式还支持 list-layers、help 等子命令,可用来查看和抽取各层:
java -Djarmode=tools -jar demo-0.0.1-SNAPSHOT.jar help
一句话总结:4.0 砍掉了启动脚本,4.1 把分层工具从 layertools 换成了 tools。两条都指向同一个方向——jar 本身越来越「纯粹」,部署编排交给容器与进程管理器。
分层 jar 与 Docker 镜像的关系
为什么要在意分层?因为 Docker 镜像是一层层叠加的,某一层没变就会命中缓存,不必重新推送或拉取。fat jar 的问题是:只要你的代码改一行,整个 19 MB 的 jar 就变了,所有依赖都得重新进镜像层。
分层把这 19 MB 拆成四层(顺序即 layers.idx 的顺序):
| 层 | 内容 | 变化频率 |
|---|---|---|
dependencies | 非快照的第三方依赖 | 几乎不变 |
spring-boot-loader | 启动器类 | 几乎不变 |
snapshot-dependencies | 快照版本依赖 | 偶尔变 |
application | 你的 class 与资源 | 每次改代码都变 |
BOOT-INF/layers.idx 就记录了这个划分。用 tools 模式抽取后,每层是一个独立目录,Dockerfile 按层 COPY,改代码时只有 application 层失效,依赖层全部命中缓存,镜像构建和推送都能快一个数量级。完整的 Dockerfile 写法留到实战卷展开,这里先把「分层是为了镜像缓存」这个因果关系记住。
spring-boot-maven-plugin 关键配置项
打包行为由 spring-boot-maven-plugin 控制。常见配置:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.demo.DemoApplication</mainClass>
<layers>
<enabled>true</enabled>
</layers>
<excludeGroupIds>org.projectlombok</excludeGroupIds>
</configuration>
</plugin>
| 配置项 | 作用 |
|---|---|
mainClass | 显式指定启动类,多主类时必填 |
layers.enabled | 是否生成分层 jar(默认 true) |
excludeGroupIds | 排除某些依赖,不放进 fat jar |
includeOptional | 是否把 optional 依赖打进 jar(见下方提醒) |
classifier | 给可执行 jar 加后缀,保留原 jar 为默认产物 |
一条 4.0 的行为变更要特别注意:Maven 的 optional 依赖默认不再打进 fat jar。如果你的项目依赖某个 optional 的库且需要它出现在运行时,必须显式加 <includeOptional>true</includeOptional>,否则会以 NoClassDefFoundError 的形式在运行时暴露。
常见坑
- 误把
.original当产物发布:它不含依赖,java -jar会失败。认准不带.original的那个。 - 用
java -cp app.jar启动:类加载器不认识嵌套 jar,必须用java -jar。 - 没配
spring-boot-maven-plugin:打出的就是普通 jar,MANIFEST 里没有Start-Class,java -jar报no main manifest attribute。 - 改代码后忘记重新
package:java -jar跑的仍是旧产物。发布前养成clean package的习惯。 - 继续使用 3.x 的
layertools命令:4.1 已移除,改用jarmode=tools。 - 期望
./app.jar直接启动:4.0 已移除启动脚本,只能java -jar。
小结
本节把 demo 从「开发态可运行」推进到「可发布」:mvn clean package 产出可执行 jar,它由 BOOT-INF/classes(你的代码)、BOOT-INF/lib(嵌套依赖)、JarLauncher(启动器)和一份写明 Main-Class/Start-Class 的 MANIFEST 组成;java -jar 即可独立运行。两条 4.x 变更务必记住:4.0 移除了嵌入式启动脚本,4.1 用 tools 取代了 layertools;而分层 jar 的 layers.idx 与四层划分,正是为了 Docker 镜像缓存,这条线索会在实战卷继续。
至此第 3 章完成:从写第一个接口,到理解应用如何启动,再到打包发布。接下来该回头看清 @SpringBootApplication 这个注解背后到底藏着什么——自动配置是如何发生的,这正是第 4 章的主题。
阅读导航:上一节:3.2 内嵌服务器与启动流程 · 下一节:4.1 拆解 @SpringBootApplication 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。