基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

为什么 Spring Boot 打包后的 JAR 可以直接运行(原理是什么)?

知识点图片

Spring Boot 打包后的 JAR 包之所以能直接运行(使用 java -jar),是因为它采用了一种特殊的打包结构,被称为 "Fat JAR" (或 Uber JAR),并内置了一套自定义的 类加载机制 (Class Loading Mechanism)

核心原理可以概括为以下三点:

  1. 特殊的目录结构:将应用代码和依赖 JAR 包分开存放。
  2. Manifest 文件重定向:入口类不是你的业务代码,而是 Spring Boot 提供的启动器。
  3. 自定义类加载器:解决了“JAR 包里嵌套 JAR 包”无法被 JVM 默认加载的问题。

下面详细解析其内部原理:


1. 内部结构(解压 JAR 包看一看)

如果你把 Spring Boot 打包好的 .jar 文件解压,你会看到以下目录结构:

plaintext
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 在这里做了一个关键的“欺骗”:

plaintext
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,通常有两种做法,但都有缺陷:

  1. Shade (解压重组):把所有依赖 JAR 包解压,把里面的 .class 文件和你的代码混在一起重新打包。
    • 缺点:文件同名冲突(不同库可能有同名文件),破坏签名。
  2. 使用第三方插件 (如 OneJar)
    • 缺点:不是官方标准,支持力度不一。

Spring Boot 的做法(嵌套 JAR)优势在于:

  • 保持依赖完整性:不需要解压第三方 JAR,直接原样放入 BOOT-INF/lib,避免了文件覆盖冲突。
  • 标准化:通过 spring-boot-maven-plugin 插件一键生成,统一了打包和运行方式。

总结

Spring Boot JAR 能直接运行,本质上是一个 “特洛伊木马”

  1. 外壳是 JarLauncher(JVM 以为它在运行这个)。
  2. 肚子里藏着 业务代码依赖 JAR
  3. JarLauncher 启动后,利用自定义的 类加载器 把肚子里的东西加载进内存,最后通过 反射 启动你的程序。
00:00
00:00