为什么 Spring Boot 打包后的 JAR 可以直接运行(原理是什么)?
Spring Boot 打包后的 JAR 包之所以能直接运行(使用 java -jar),是因为它采用了一种特殊的打包结构,被称为 "Fat JAR" (或 Uber JAR),并内置了一套自定义的 类加载机制 (Class Loading Mechanism)。
核心原理可以概括为以下三点:
- 特殊的目录结构:将应用代码和依赖 JAR 包分开存放。
- Manifest 文件重定向:入口类不是你的业务代码,而是 Spring Boot 提供的启动器。
- 自定义类加载器:解决了“JAR 包里嵌套 JAR 包”无法被 JVM 默认加载的问题。
下面详细解析其内部原理:
1. 内部结构(解压 JAR 包看一看)
如果你把 Spring Boot 打包好的 .jar 文件解压,你会看到以下目录结构:
example-app.jar
├── META-INF
│ └── MANIFEST.MF <-- 关键配置文件
├── org
│ └── springframework
│ └── boot
│ └── loader <-- Spring Boot 提供的引导程序 (Loader)
├── BOOT-INF
│ ├── classes <-- 你的业务代码 (.class 文件)
│ └── lib <-- 第三方依赖 (mysql.jar, spring-core.jar 等)
- 普通 JAR:通常把所有
.class文件放在根目录下。 - Spring Boot JAR:
org/springframework/boot/loader/:这是 Spring Boot 注入的一套“引导程序”,负责初始化环境。BOOT-INF/classes/:你的代码被挪到了这里。BOOT-INF/lib/:所有的 Maven/Gradle 依赖都被放在这里(嵌套 JAR)。
2. 核心机制:MANIFEST.MF 的“偷梁换柱”
当你执行 java -jar app.jar 时,JVM 会去读取 META-INF/MANIFEST.MF 文件。Spring Boot 在这里做了一个关键的“欺骗”:
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher <-- 1. 真正的物理入口
Start-Class: com.example.demo.DemoApplication <-- 2. 你的业务入口
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Main-Class:JVM 实际上启动的是JarLauncher(Spring Boot 提供的类),而不是你写的DemoApplication。Start-Class:这是 Spring Boot 自定义的属性,记录了你真正的main方法所在的类。
3. 运行流程(JarLauncher 是如何工作的)
整个启动过程可以分为以下几步:
第一步:JVM 启动 JarLauncher
JVM 启动,加载 org.springframework.boot.loader.JarLauncher 并执行其 main 方法。此时,JVM 只能看到 loader 目录下的类,还看不到 BOOT-INF 下的业务代码和依赖。
第二步:创建自定义类加载器 (LaunchedURLClassLoader)
Java 默认的类加载器无法加载 JAR 包内部的 JAR 包(即 BOOT-INF/lib/*.jar)。
为了解决这个问题,JarLauncher 会创建一个特殊的类加载器:LaunchedURLClassLoader。
这个加载器支持特殊的路径协议(Jar in Jar),能够读取:
BOOT-INF/classes/中的类。BOOT-INF/lib/中嵌套 JAR 包里的类。
第三步:反射调用用户的 Main 方法
环境准备好后,JarLauncher 会读取 MANIFEST.MF 中的 Start-Class 属性(即你的 com.example.demo.DemoApplication)。
然后,它通过反射 (Reflection) 的方式,使用刚才创建的 LaunchedURLClassLoader 加载你的主类,并调用其 main(String[] args) 方法。
此时,控制权才正式移交给你的 Spring Boot 应用程序。
4. 为什么要这么做?(解决什么痛点)
在 Spring Boot 出现之前,要打一个包含所有依赖的可执行 JAR,通常有两种做法,但都有缺陷:
- Shade (解压重组):把所有依赖 JAR 包解压,把里面的
.class文件和你的代码混在一起重新打包。- 缺点:文件同名冲突(不同库可能有同名文件),破坏签名。
- 使用第三方插件 (如 OneJar):
- 缺点:不是官方标准,支持力度不一。
Spring Boot 的做法(嵌套 JAR)优势在于:
- 保持依赖完整性:不需要解压第三方 JAR,直接原样放入
BOOT-INF/lib,避免了文件覆盖冲突。 - 标准化:通过
spring-boot-maven-plugin插件一键生成,统一了打包和运行方式。
总结
Spring Boot JAR 能直接运行,本质上是一个 “特洛伊木马”:
- 外壳是
JarLauncher(JVM 以为它在运行这个)。 - 肚子里藏着 业务代码 和 依赖 JAR。
JarLauncher启动后,利用自定义的 类加载器 把肚子里的东西加载进内存,最后通过 反射 启动你的程序。