《Spring Boot 入门》3.3 打包成可执行 jar

用 mvn package 把 demo 打成可执行 jar,拆解 BOOT-INF/classes、BOOT-INF/lib 与 JarLauncher 的内部结构,讲清 4.0 移除可执行 jar 启动脚本、4.1 用 tools 取代 layertools 两条变更,并说明分层 jar 与 Docker 镜像的关系。

本节目标:用 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 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计