背景
Spring Boot 把应用打成单个可执行 JAR,java -jar app.jar 即可启动内嵌 Tomcat(或 Jetty/Undertow)与全部依赖。这与传统「WAR 部署到外部 Tomcat、classpath 由容器管理」不同,也不同于 Maven jar-with-dependencies 那种把依赖 class 解压合并进一个 jar 的做法。
本文说明 Spring Boot 可执行 JAR 的内部布局、JarLauncher 如何启动应用,以及 repackage / bootJar 如何生成该产物。容器镜像、分层 JAR、Buildpack 等部署话题见 Spring Boot Container Packaging;框架层面的「内嵌服务器 + 可执行 JAR」见 Spring Boot。
Thin jar 与 fat jar
Fat jar、uber jar、executable jar 在 Spring Boot 语境下指同一件事:一个「全包」可执行 jar。Fat jar 与 uber jar 只是不同叫法(uber 来自德语 über,意为「之上 / 全部」);Spring 官方文档更常写 executable jar。
与 Maven/Gradle 默认打出的普通 jar(常称 thin jar / plain jar)对比如下:
| 普通 jar(thin / plain jar) | Fat jar(uber jar / executable jar) | |
|---|---|---|
| 内容 | 通常只有项目自己的 .class 与资源 |
应用代码 + 全部依赖 + spring-boot-loader(及内嵌服务器等) |
| 依赖在哪 | 不在 jar 内,需 classpath 或外部 lib | 嵌在 BOOT-INF/lib/*.jar |
java -jar |
一般不能直接跑(缺依赖) | 可以,一条命令启动 |
| Spring Boot 谁生成 | jar 任务 / mvn package 的原始产物(常被重命名为 *.jar.original) |
repackage / bootJar 的产物 |
| 典型用途 | 作为库被别的项目依赖 | 部署、java -jar 运行、作为容器镜像输入 |
布局与 MANIFEST.MF
Spring Boot 可执行 JAR 使用固定目录约定,而不是把依赖 class 全部解压到 jar 根目录:
app.jar
├── META-INF/MANIFEST.MF # 启动元数据,关键项见下表
├── org/springframework/boot/loader/ # spring-boot-loader 启动类
└── BOOT-INF/ # Spring Boot 专有布局,非 Java/JAR 标准
├── classes/ # 你的代码、application.yml、static/ 等
└── lib/ # 第三方依赖,每个依赖仍是独立 .jar
BOOT-INF/、BOOT-INF/classes/、BOOT-INF/lib/ 是 Spring Boot 为可执行 JAR 定义的约定,不是 JAR 文件格式 或 JDK 规范的一部分。标准 JAR 只约定 META-INF/MANIFEST.MF 等元数据,应用 class 通常直接放在 jar 根目录的包路径下;标准 classpath 也不支持「jar 内嵌套 jar 自动可用」,因此需要 JarLauncher 与自定义 ClassLoader。
META-INF/MANIFEST.MF 里与启动相关的关键项:
| 属性 | 含义 |
|---|---|
Main-Class |
JVM 执行 java -jar 时加载的类,Spring Boot 设为 org.springframework.boot.loader.launch.JarLauncher |
Start-Class |
你的业务主类(带 public static void main 的 @SpringBootApplication 类) |
可用下面命令查看打包结果:
unzip -p target/myapp.jar META-INF/MANIFEST.MF
Main-Class 与 Start-Class 的分工是 Spring Boot 可执行 JAR 的核心:Main-Class 不是业务类,而是 loader 入口。MANIFEST.MF 通用字段说明见 MANIFEST.MF。
JarLauncher 启动链
执行 java -jar app.jar 时,JVM 只认 MANIFEST.MF 里的 Main-Class,因此实际入口是 JarLauncher,而不是你的 MyApplication。
java -jar app.jar
→ JVM 启动 JarLauncher.main()
→ 自定义 ClassLoader 挂载 BOOT-INF/classes 与 BOOT-INF/lib 内嵌套 jar
→ 读取 Start-Class,反射调用业务 main()
→ SpringApplication.run(...) 启动内嵌服务器与 Spring 容器
JarLauncher 来自 spring-boot-loader 模块,打进可执行 jar 的 org/springframework/boot/loader/ 目录。它解决的是标准 JAR 布局的限制:应用类在 BOOT-INF/classes/,依赖在 BOOT-INF/lib/*.jar 里仍是嵌套 jar,普通 classpath 无法直接加载;loader 在启动阶段把这些路径挂到可搜索的 classpath 上,再转调 Start-Class。
这与 Spring Boot §启动应用 里「一条 java -jar 拉起内嵌 Tomcat」的用户体验一致;内嵌 Tomcat 的启动发生在 SpringApplication.run 之后,属于业务主类一侧的逻辑。
启动全流程
JVM 执行 java -jar app.jar 时不会先把 jar 解压到磁盘,而是用 ZIP 随机访问直接读取。
JVM 如何定位 MANIFEST.MF
jar 本质是 ZIP,ZIP 把中央目录(Central Directory)放在文件末尾,所以 JVM 不必从头读:
① seek 到文件末尾 → 读 End of Central Directory(EOCD,定长)
② 从 EOCD 拿到中央目录偏移 → seek 过去
③ 在目录中找到 META-INF/MANIFEST.MF 条目 → 得到其字节偏移
④ seek 到该偏移 → Deflate 解压 → 读出 MANIFEST.MF
只需 3 次 seek,不触碰其它文件。ZIP「每个文件独立压缩、可随机访问」的特性见 Compress and Extract §归档格式与随机访问。
类按需懒加载
确定 Main-Class(JarLauncher)后整体时序:
java -jar app.jar
↓ OS fork 进程,加载 libjvm.so(JVM 主体)
↓ JVM 初始化堆 / GC / JIT
↓ 3 次 seek 读 MANIFEST.MF(不解压整包)
↓ 加载顶层 JarLauncher(标准 ClassLoader 可直接读)
↓ JarLauncher 用 NestedFileSystemProvider 挂载 BOOT-INF/lib/ 内嵌 jar
↓ 反射调用 Start-Class.main() → SpringApplication.run()
→ 创建 ApplicationContext → refresh() → 启动内嵌服务器
.class 文件按需懒加载——只有某个类首次被用到时才从 jar 读出并解压,磁盘上的 jar 全程保持压缩。libjvm.so 与 java 启动器的关系见 HotSpot §libjvm.so 与 JVM 主体。
SpringApplication.run() 内部的 Spring 装配分三段(详见 Spring IoC §refresh()):扫描登记 Bean 定义(用 ASM 读字节码元数据,不加载类)→ 实例化单例 → 注入依赖。
META-INF 里还有什么
META-INF 把 jar 从「哑压缩包」变成自描述模块。可执行 fat jar 的 META-INF 常见内容:
META-INF/
├── MANIFEST.MF # 启动元数据,见上文
├── maven/<group>/<artifact>/
│ ├── pom.xml # 打包时拷入的原始 pom
│ └── pom.properties # groupId/artifactId/version,便于快速查询
└── services/
└── java.nio.file.spi.FileSystemProvider # Java SPI 注册
maven/...:Maven 打包时嵌入,mvn dependency:tree等工具据此识别 classpath 上的构件。services/:Java SPI 注册目录。
其中该 SPI 文件的内容为:
org.springframework.boot.loader.nio.file.NestedFileSystemProvider
标准 JVM 无法表示「jar 里再嵌 jar」的路径。NestedFileSystemProvider 向 JVM 注册了一个新的 nested: URI scheme,使下面这种路径可用:
nested:/path/to/app.jar/!BOOT-INF/lib/spring-core-6.x.jar
这让 JarLauncher 能把 BOOT-INF/lib/ 下每个嵌套 jar 当作独立文件系统打开并加载类——正是 fat jar 无需解压即可运行的技术基础。MANIFEST.MF 各字段含义见 MANIFEST.MF。
Fat jar 与 Web 服务
Fat jar 不限于纯 REST API:java -jar 启动后会拉起内嵌 Web 容器(默认 Tomcat 监听 8080),同时提供 HTTP API 与静态资源。
src/main/resources/ 下的内容会打进 BOOT-INF/classes/,例如:
src/main/resources/
├── application.yml
├── static/ # 静态前端:html、js、css、图片
│ ├── index.html
│ └── js/app.js
└── templates/ # Thymeleaf 等服务端模板(可选)
打包后对应 BOOT-INF/classes/static/index.html 等路径。浏览器访问 http://localhost:8080/ 或 /index.html 可拿到页面;/api/... 由 @RestController 处理。前后端分离项目也常把 React/Vue 的 dist/ 在构建时拷入 static/,与 API 打进同一个 jar,一个进程同时提供接口与静态站点。
| 前端形态 | 能否打进 fat jar | 说明 |
|---|---|---|
| 纯静态 HTML/JS/CSS | 是 | 放 resources/static/ |
| SPA 构建产物(React/Vue 等) | 是 | 构建输出拷入 static/ |
| Thymeleaf 等服务端模板 | 是 | 模板在 resources/templates/ |
| JSP | 可执行 JAR 基本不行 | 需打 WAR;见 Spring MVC §Spring Boot 与 JSP |
WAR 只是另一种打包格式,并不等同于「带前端页面」——纯 JSON 的 REST API 也可以打成 WAR 部署到外部 Tomcat,见下一节。
可执行 WAR 与 JAR 选型
除可执行 JAR 外,Spring Boot 还可打成 可执行 WAR:在 Servlet 规范的 WEB-INF/ 布局上写入 WarLauncher,MANIFEST.MF 的 Main-Class 指向 org.springframework.boot.loader.launch.WarLauncher。
app.war
├── META-INF/MANIFEST.MF # Main-Class → WarLauncher
├── org/springframework/boot/loader/
└── WEB-INF/ # Servlet 规范目录(WEB-INF 本身来自 Servlet 规范)
├── classes/
├── lib/
└── lib-provided/ # provided 依赖,供外部容器使用
可执行 WAR 支持两种部署方式:
java -jar app.war—— 与 JAR 类似,内嵌 Tomcat 独立进程启动- 拷贝到外部 Tomcat 的
webapps/—— 由容器加载,此时内嵌 Tomcat 依赖以provided形式放在WEB-INF/lib-provided/,不打进运行时 classpath
Tomcat 只提供 Servlet 容器;返回 JSON 还是 HTML 对容器无区别。RESTful API 完全可以打成 WAR 部署在外部 Tomcat,传统 Spring MVC 时代很常见。Spring Boot 默认推 JAR,是因为微服务场景更倾向「每个应用独立进程、java -jar 即可」,而不是 WAR 不能跑 API。
| 可执行 JAR | 可执行 WAR | |
|---|---|---|
| 布局 | BOOT-INF/(Spring Boot 约定) |
WEB-INF/(Servlet 规范) |
| Launcher | JarLauncher |
WarLauncher |
java -jar |
是 | 是 |
| 外部 Tomcat 部署 | 否 | 是 |
static/ 静态资源 |
是 | 是 |
JSP(src/main/webapp/) |
基本不行 | 可以 |
日常 REST 服务默认 JarLauncher + JAR 即可。需要外部 Tomcat、或必须用 JSP 时再选 WAR。PropertiesLauncher 支持更灵活的 classpath(如外部 lib/ 目录),需在 MANIFEST.MF 配置 Loader 等属性,使用较少。
构建:repackage 与 bootJar
可执行 JAR 由 Spring Boot 打包插件在默认 jar / bootJar 任务之上二次处理生成:
- Maven:
spring-boot-maven-plugin的repackagegoal(使用spring-boot-starter-parent时通常已绑定到package阶段) - Gradle:
bootJar任务(build/assemble会自动执行)
WAR 对应 Maven 的 repackage + <packaging>war</packaging>,或 Gradle 的 bootWar 任务。
./mvnw package
java -jar target/myapp.jar
# Gradle
./gradlew bootJar
java -jar build/libs/myapp.jar
repackage 会把 Maven 默认 jar 任务产出的 thin jar 重命名为 *.jar.original,再写出可执行 fat jar 覆盖最终文件名。若项目要同时作为库被依赖,应保留 thin jar(*.jar.original),不要把 fat jar 发布到 Maven 仓库供他人 compile 依赖。
官方打包说明:
- Maven:https://docs.spring.io/spring-boot/maven-plugin/packaging.html
- Gradle:https://docs.spring.io/spring-boot/gradle-plugin/packaging.html
与普通可执行 jar 的区别
在 Spring Boot 之前或之外,Java 生态里也有「可执行 fat jar」,常见做法不同:
| 方式 | 机制 | 与 Spring Boot 的差异 |
|---|---|---|
Maven Assembly jar-with-dependencies |
依赖解压合并,Main-Class 直接指向业务类 |
无 BOOT-INF、无嵌套 lib jar、无 JarLauncher |
| Maven Shade Plugin | 重定位/合并 class,防冲突 | 同上,且侧重 uber jar 冲突处理 |
Gradle application 插件 |
生成带脚本与 lib 目录的分发物 | 常是目录 + 启动脚本,而非单 jar 嵌套 lib |
本站笔记:Maven 可执行 jar、Gradle application 插件、maven-shade-plugin。
Maven Shade 那种「把所有依赖的 class 解压平铺进一层」的做法有几个明显问题,正是 Spring Boot 保留嵌套 jar 的原因:
| 问题 | 说明 |
|---|---|
| 文件冲突 | 多个 jar 含同名文件(LICENSE、META-INF/spring.factories),合并后只剩一份 |
| SPI 失效 | META-INF/services/ 下同名文件相互覆盖,丢失实现 |
| 签名失效 | 解压已签名 jar 会破坏其签名 |
| 可追溯性差 | 无法判断某个 class 来自哪个依赖的哪个版本 |
Spring Boot 把依赖 jar 原样放在 BOOT-INF/lib/,靠 NestedFileSystemProvider 加自定义 ClassLoader 读取,规避了上述全部问题;这也是为了加快打包、并支持后续分层 JAR(layers.idx)与 Buildpack 切层——分层与容器化见 Spring Boot Container Packaging。
参考
- Spring Boot
- Spring MVC
- Spring Boot Container Packaging
- MANIFEST.MF
- Maven 可执行 jar
- Spring Boot Packaging 官方文档
维护记录
| 时间 | 修改内容 | 原因 |
|---|---|---|
| 2026-06-26 | 精简布局注释中 Main-Class / Start-Class 说明 |
与下方 MANIFEST.MF 表格重复,只在表格处保留一处 |
| 2026-06-27 | 新增「启动全流程」(ZIP 随机访问、类懒加载)、「META-INF 里还有什么」(maven/、SPI、NestedFileSystemProvider);补充扁平解压的问题表 | 合并 comments-tree 启动打包文档相关章节 |