← 返回题目列表

Spring Boot 的可执行 jar 是怎么运行的?为什么能 java -jar 直接启动?

中等 第 18 / 25 题 更新于 2026/07/26
可执行jarfat jarJarLauncher类加载

简化版

Spring Boot 打出的是一个 fat jar(胖 jar / 可执行 jar)——它把你的代码、所有依赖 jar、以及一个「启动引导器」全打进一个 jar,所以 java -jar app.jar 就能直接跑,不用配 classpath、不用外部 Tomcat。它和普通 jar 不同:内部有特殊结构(BOOT-INF/classes 放你的类、BOOT-INF/lib 放依赖 jar),且 MANIFEST.MF 里的 Main-Class 不是你的启动类,而是 Spring Boot 的 JarLauncherJarLauncher 用一个自定义类加载器(能加载「jar 里嵌套的 jar」,标准 Java 做不到),把依赖都加载进来,再反射调用你真正的 main 方法。这就是 fat jar「一个包搞定一切」的原理。

详细版

fat jar 的内部结构

app.jar
├── META-INF/
│   └── MANIFEST.MF          # Main-Class: org.springframework.boot.loader.JarLauncher
│                            # Start-Class: com.example.Application(你真正的启动类)
├── org/springframework/boot/loader/   # Spring Boot 的启动引导器(JarLauncher 等)
├── BOOT-INF/
│   ├── classes/             # ★ 你自己的代码(编译后的 .class)和资源
│   │   └── com/example/...
│   └── lib/                 # ★ 所有依赖 jar(spring-core.jar、mysql.jar…)
│       └── *.jar            # 一堆嵌套的 jar

启动流程

java -jar app.jar
  → JVM 读 MANIFEST.MF 的 Main-Class → 是 JarLauncher(不是你的类!)
  → JarLauncher 运行:
    1. 创建自定义类加载器 LaunchedURLClassLoader
       (能加载 BOOT-INF/lib 下嵌套 jar 里的类——标准 ClassLoader 做不到)
    2. 读 MANIFEST.MF 的 Start-Class → 你真正的启动类 com.example.Application
    3. 用自定义类加载器加载你的启动类,反射调用它的 main 方法
  → 你的 main 里 SpringApplication.run() → 正常启动 Spring Boot

为什么需要 JarLauncher 和自定义类加载器

标准 Java 规范:jar 里不能直接加载"嵌套 jar(jar in jar)"里的类
  → java -jar 只能加载本 jar 顶层的类,加载不了 BOOT-INF/lib/xxx.jar 里的类
Spring Boot 的解法:
  → 自定义 LaunchedURLClassLoader,让它能读取并加载嵌套 jar 里的类
  → 由 JarLauncher 先建好这个类加载器,再启动你的应用

⚠️ fat jar 是 Spring Boot Maven/Gradle 插件(spring-boot-maven-pluginrepackage二次打包产生的——普通 mvn package 先打出一个普通 jar,插件再把它「重新打包」成 fat jar(塞入依赖、加启动器、改 MANIFEST)。所以打包日志里会看到普通 jar 和一个 .jar.original(原始 jar 被备份了)。没有这个插件,打出的就是普通 jar,java -jar 会报「找不到主类」或缺依赖。

完整版教学

一、fat jar 解决什么:一个包搞定部署

传统 Java Web 应用部署很繁琐——打成 war 包,扔进外部 Tomcat,还要配好 Tomcat、管理依赖。Spring Boot 想让部署变简单:打成一个 jar,java -jar 就能跑

传统方式:war 包 + 外部 Tomcat + 配 classpath + 管依赖  → 步骤多、易出错
Spring Boot:一个 fat jar,java -jar app.jar 直接启动  → 一步到位

要做到「一个包搞定」,这个 jar 必须自包含(self-contained)——包含你的代码、所有依赖、以及内嵌的 Web 服务器(Tomcat)。这就是 fat jar(也叫 uber jar、胖 jar)的含义:把运行所需的一切都塞进一个 jar。理解「fat jar = 自包含的可执行 jar」,就理解了为什么它能脱离外部容器独立运行——因为它自己就带了运行环境。

二、fat jar 和普通 jar 的结构区别

普通 jar 和 fat jar 结构完全不同,这是理解的关键:

普通 jar(依赖不在里面):
  com/example/*.class      # 只有你的类
  META-INF/MANIFEST.MF     # Main-Class 指向你的启动类
  → 运行时依赖要靠外部 classpath 提供,缺依赖就报 NoClassDefFound

Spring Boot fat jar(依赖全在里面):
  org/springframework/boot/loader/*   # 启动引导器
  BOOT-INF/classes/         # 你的类和资源
  BOOT-INF/lib/*.jar        # ★ 所有依赖 jar 都打进来了!
  META-INF/MANIFEST.MF      # Main-Class 指向 JarLauncher(不是你的类)

两个关键差异:① 依赖 jar 被打进 BOOT-INF/lib(普通 jar 没有依赖);Main-Class 是 Spring Boot 的 JarLauncher 而非你的启动类(你的启动类在 Start-Class 里)。为什么你的类放在 BOOT-INF/classes 而不是 jar 根目录?就是为了和启动引导器(在根目录的 org/springframework/boot/loader)分开,让 JarLauncher 能先启动、再用特殊方式加载 BOOT-INF 里的东西。

三、核心难题:jar 里的 jar 加载不了

fat jar 面临一个 Java 规范层面的难题:标准 Java 无法直接加载「嵌套 jar(jar in jar)」里的类

BOOT-INF/lib/ 下是一堆完整的 .jar 文件(spring-core.jar 等)
标准 Java 的 URLClassLoader 只能加载:
  - 文件系统上独立的 .jar
  - jar 里顶层的 .class
它无法加载「一个 jar 内部嵌套的另一个 jar」里的 .class!
  → 如果直接把依赖 jar 塞进去,标准类加载器根本读不到里面的类

有两种解决思路:方案 A(Shade/uber jar)——把所有依赖 jar 解压,把里面的 .class 全平铺到一个 jar 里(不保留 jar 结构)。这样能用标准类加载器,但会丢失依赖的独立性、可能有文件冲突。方案 B(Spring Boot 的做法)——保留依赖 jar 的完整结构(BOOT-INF/lib/*.jar),自己写一个能加载嵌套 jar 的类加载器。Spring Boot 选了 B,因为它保留了依赖的完整性和可追溯性。这就引出了 JarLauncher 和自定义类加载器。

四、JarLauncher 与自定义类加载器

Spring Boot 的解法核心是两个东西:JarLauncher(启动引导器)+ LaunchedURLClassLoader(自定义类加载器)

java -jar app.jar 的完整过程:
  1. JVM 看 MANIFEST.MF 的 Main-Class = JarLauncher → 先运行 JarLauncher
     (JarLauncher 的类在 jar 根目录 org/springframework/boot/loader/ 下,
      标准类加载器能加载它——所以它放根目录不放 BOOT-INF)
  2. JarLauncher 创建 LaunchedURLClassLoader:
     这个自定义类加载器"认识" BOOT-INF/lib 下的嵌套 jar,
     能从「jar 里的 jar」中读取并加载 .class
  3. JarLauncher 读 Start-Class = com.example.Application(你的真启动类)
  4. 用 LaunchedURLClassLoader 加载你的启动类,反射调它的 main()
  5. 你的 main → SpringApplication.run() → 应用启动

关键设计:JarLauncher 自己放在 jar 根目录(标准类加载器能加载它),它负责「搭好能加载嵌套 jar 的环境」,再把控制权交给你的应用。所以 Main-Class 是 JarLauncher(引导者)、Start-Class 是你的类(真正的应用入口)——两级启动,先引导再运行。这就是「为什么 java -jar 能启动一个自包含 jar」的完整答案。

五、repackage:fat jar 是怎么打出来的

fat jar 不是普通 mvn package 直接产生的,而是 Spring Boot 插件二次打包的结果:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <!-- 它绑定了 repackage 目标 -->
</plugin>
打包过程:
  1. mvn package 先正常编译、打出一个"普通 jar"(只含你的类)
  2. spring-boot-maven-plugin 的 repackage 目标接手,把普通 jar"重新打包":
     - 把普通 jar 里的类挪到 BOOT-INF/classes
     - 把所有依赖 jar 塞进 BOOT-INF/lib
     - 把 Spring Boot 启动引导器(loader)加进来
     - 改写 MANIFEST.MF:Main-Class=JarLauncher, Start-Class=你的类
  3. 产出:
     - app.jar(fat jar,可执行)
     - app.jar.original(原始普通 jar 的备份)

所以「没加 spring-boot-maven-plugin 就打不出可执行 jar」——普通 mvn package 只能打普通 jar(java -jar 会因缺依赖或没有正确 Main-Class 而失败)。理解「fat jar 是 repackage 二次加工的产物」,就明白了打包日志里为什么有 .jar.original(那是被替换前的普通 jar)。

六、fat jar 的取舍与相关部署方式

fat jar 好用,但也有取舍,以及几种相关部署方式要理清:

方式说明适用
fat jar(默认)一个自包含 jar,java -jar 直接跑微服务、容器化部署
war 包传统方式,部署到外部 Tomcat需要复用外部容器、传统运维
分层 jar(layered jar)把 fat jar 按变化频率分层(依赖层、代码层)Docker 镜像分层缓存,加快构建

fat jar 的代价① 体积大(含所有依赖,几十到上百 MB);② 依赖更新要重新打整个包。针对 Docker 场景,Spring Boot 2.3+ 支持分层 jar(layered jar)——把 fat jar 拆成「依赖层(很少变)、代码层(经常变)」等,配合 Docker 镜像分层,依赖没变时构建镜像能复用缓存层,只重传变化的代码层,大幅加快镜像构建和推送。这是 fat jar 在容器时代的优化。也可以打成 war 部署到外部 Tomcat(需要继承 SpringBootServletInitializer),适合要复用现有容器的传统环境。

记忆钩子:「fat jar 自包含(BOOT-INF/classes 你的类 + BOOT-INF/lib 依赖 jar + loader 引导器);Main-Class 是 JarLauncher 不是你的类(你的在 Start-Class);标准 Java 加载不了嵌套 jar,靠自定义 LaunchedURLClassLoader 解决;由 spring-boot-maven-plugin 的 repackage 二次打包产生;Docker 用分层 jar 优化」

七、常见误区与追问

  • 误区:Spring Boot 的可执行 jar 和普通 jar 结构一样。 完全不同——fat jar 有 BOOT-INF/classes(你的类)、BOOT-INF/lib(依赖 jar)、loader(引导器),Main-Class 是 JarLauncher 而非你的启动类。
  • 误区:java -jar 直接运行的是你的启动类。 运行的是 Main-Class 指定的 JarLauncher,它搭好类加载环境后才反射调用 Start-Class(你的启动类)的 main。
  • 误区:普通 mvn package 就能打出可执行 jar。 需要 spring-boot-maven-plugin 的 repackage 二次打包;普通 package 只出普通 jar,java -jar 会缺依赖或找不到主类。
  • 误区:标准 Java 能加载 jar 里嵌套的 jar。 不能,标准 URLClassLoader 加载不了「jar in jar」里的类;Spring Boot 用自定义 LaunchedURLClassLoader 才实现。
  • 追问:为什么 Main-Class 不直接是你的启动类? 因为你的类和依赖在 BOOT-INF 里、需要特殊类加载器才能加载;先让 JarLauncher(在 jar 根目录、标准类加载器能加载它)启动、建好 LaunchedURLClassLoader,再加载你的类。
  • 追问:fat jar 体积大、依赖更新慢怎么优化? Spring Boot 2.3+ 的分层 jar(layered jar)把 jar 按变化频率分层,配合 Docker 镜像分层缓存,依赖没变时复用缓存层,只重建变化的代码层。
  • 追问:Spring Boot 能打成 war 部署到外部 Tomcat 吗? 能,启动类继承 SpringBootServletInitializer、打包类型改 war、内嵌 Tomcat 依赖设为 provided,即可部署到外部 Servlet 容器。

八、加强记忆

Spring Boot 的可执行 fat jar 是「自包含」的——把你的代码、所有依赖 jar、内嵌 Tomcat、启动引导器全打进一个 jar,所以 java -jar app.jar 一步启动、无需外部容器。它的结构和普通 jar 完全不同:BOOT-INF/classes(你的类)+ BOOT-INF/lib(依赖 jar)+ 根目录的 loader(引导器),且 MANIFEST.MFMain-Class 是 Spring Boot 的 JarLauncher(你的启动类在 Start-Class)。核心难题是标准 Java 加载不了「jar 里嵌套的 jar」,Spring Boot 的解法是自定义 LaunchedURLClassLoader(能读嵌套 jar),由 JarLauncher(放 jar 根目录、标准类加载器能加载它)先建好这个类加载器、再反射调用你的 main——两级启动,先引导再运行。fat jar 由 spring-boot-maven-pluginrepackage 二次打包产生(普通 package 只出普通 jar,还会留个 .jar.original 备份)。代价是体积大,Docker 场景用分层 jar 优化(按变化频率分层复用镜像缓存)。一句话「自包含 fat jar、Main-Class 是 JarLauncher、自定义类加载器读嵌套 jar、repackage 二次打包、分层 jar 优化 Docker」。