Fat JAR
一句话定位:Spring Boot 通过
spring-boot-maven-plugin将应用代码与所有依赖打包为可执行的 Fat JAR,利用JarLauncher和BOOT-INF目录结构实现依赖隔离加载,达成java -jar一键运行的部署体验。
定义与作用
传统的 Java Web 应用打包产出为 WAR 文件,其中仅包含项目自身的 WEB-INF/classes 和 WEB-INF/lib,运行时依赖外部 Servlet 容器提供标准库(如 Servlet API)。这种方式的问题在于:部署环境必须与开发环境的容器版本匹配,依赖冲突排查困难,回滚时需要替换整个容器目录。
Spring Boot 的 Fat JAR(可执行 JAR) 将以下三类内容全部打包进一个 JAR 文件:
| 内容 | 传统 WAR 位置 | Spring Boot Fat JAR 位置 | 说明 |
|---|---|---|---|
| 项目编译后的类文件 | WEB-INF/classes/ | BOOT-INF/classes/ | 业务代码、配置类 |
| 项目依赖的第三方 JAR | WEB-INF/lib/ | BOOT-INF/lib/ | Spring Framework、数据库驱动、连接池等 |
| 启动器类 | WEB-INF/web.xml 或容器控制 | META-INF/MANIFEST.MF 指定 Main-Class | JarLauncher 作为入口 |
关键差异:Fat JAR 的
BOOT-INF目录是 Spring Boot 自定义的,标准 JVM 类加载器不会自动扫描它。Spring Boot 通过自定义的JarLauncher及其类加载器LaunchedURLClassLoader实现对BOOT-INF/classes和BOOT-INF/lib/*.jar的隔离加载。
适用位置与常用属性
Fat JAR 的构建完全由 Maven 插件控制,开发者在 pom.xml 中配置 spring-boot-maven-plugin。
插件配置位置
<!-- pom.xml -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.18</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 常用配置属性 -->
<mainClass>com.feixiang.student.StudentManagementApplication</mainClass>
<layout>JAR</layout>
<excludeDevtools>true</excludeDevtools>
</configuration>
</plugin>
</plugins>
</build>
常用配置属性
| 属性 | 说明 | 典型值 |
|---|---|---|
<mainClass> | 显式指定启动类全限定名 | com.feixiang.student.StudentManagementApplication |
<layout> | 打包布局类型 | JAR(默认)、WAR、ZIP |
<excludeDevtools> | 是否排除 devtools | true(生产环境建议) |
<classifier> | 附加分类器后缀 | exec |
<executable> | Linux 下生成可执行脚本头 | true |
注意:如果 POM 继承了
spring-boot-starter-parent,则插件版本号可省略,由父 POM 统一管理。
核心原理
Fat JAR 内部结构
结构解读:
META-INF/MANIFEST.MF:Main-Class指向 Spring Boot 的JarLauncher,而非业务启动类;真正的业务主类记录在Start-Class中。org/springframework/boot/loader/:Spring Boot Loader 模块的类文件,直接位于 JAR 根目录,由标准 JVM 类加载器加载。BOOT-INF/classes/:项目本身的编译输出,标准类加载器不可见。BOOT-INF/lib/:所有 Maven 依赖的 JAR 包,标准类加载器不可见。
启动加载流程
流程解读:
java -jar命令由 JVM 的标准类加载器(AppClassLoader)加载 JAR 根目录下的JarLauncher。JarLauncher构造LaunchedURLClassLoader,将BOOT-INF/classes/和BOOT-INF/lib/下的每个 JAR 解析为 URL 资源。LaunchedURLClassLoader作为子类加载器,负责后续所有 Spring Boot 类和依赖类的加载,实现与系统类路径的隔离。- 通过反射调用
Start-Class(即业务启动类)的main方法,启动 Spring 容器。
完整示例
场景说明
飞翔科技的运维团队要求小崔将学生成绩管理系统打包为可执行 JAR,以便在测试服务器上直接 java -jar 运行。小崔需要验证打包后的 JAR 结构正确,且能独立启动。
操作前:使用普通 Maven 打包
<!-- 错误示范:pom.xml 中未配置 spring-boot-maven-plugin -->
<build>
<plugins>
<!-- 只有 maven-jar-plugin,这是普通 JAR 打包 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
</plugin>
</plugins>
</build>
执行打包:
$ mvn clean package
生成的 target/student-app-1.0.0.jar 仅包含项目自身的类文件,没有 Spring Boot Loader,也没有 BOOT-INF/lib 目录。运行结果:
$ java -jar target/student-app-1.0.0.jar
no main manifest attribute, in target/student-app-1.0.0.jar
或即使指定了主类,也会因缺少依赖而抛出 ClassNotFoundException: org.springframework.boot.SpringApplication。
使用该特性的完整代码
小崔在 pom.xml 中引入正确的插件:
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<groupId>com.feixiang</groupId>
<artifactId>student-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.feixiang.student.StudentManagementApplication</mainClass>
<excludeDevtools>true</excludeDevtools>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
执行打包:
$ mvn clean package
操作后运行结果及分析
打包成功后,目录结构如下:
target/
├── student-app-1.0.0.jar ← 原始 JAR(maven-jar-plugin 生成)
└── student-app-1.0.0.jar.original ← repackage 前的备份
注意:
spring-boot-maven-plugin的repackage目标会在maven-jar-plugin打包完成后,将其重命名为.jar.original,然后生成新的 Fat JAR 覆盖原文件名。
查看 JAR 内部结构:
$ jar tf target/student-app-1.0.0.jar | head -30
META-INF/
META-INF/MANIFEST.MF
org/
org/springframework/
org/springframework/boot/
org/springframework/boot/loader/
org/springframework/boot/loader/JarLauncher.class
BOOT-INF/
BOOT-INF/classes/
BOOT-INF/classes/com/
BOOT-INF/classes/com/feixiang/
BOOT-INF/classes/com/feixiang/student/
BOOT-INF/classes/com/feixiang/student/StudentManagementApplication.class
BOOT-INF/classes/application.yml
BOOT-INF/lib/
BOOT-INF/lib/spring-boot-2.7.18.jar
BOOT-INF/lib/spring-boot-starter-web-2.7.18.jar
BOOT-INF/lib/spring-web-5.3.31.jar
BOOT-INF/lib/spring-webmvc-5.3.31.jar
BOOT-INF/lib/tomcat-embed-core-9.0.83.jar
...
启动验证:
$ java -jar target/student-app-1.0.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.18)
2024-05-20 10:30:15.456 INFO 9876 --- [main] o.s.b.w.e.t.TomcatWebServer : Tomcat started on port(s): 8080 (http)
分析:
JarLauncher被 JVM 正确加载,进而启动LaunchedURLClassLoader。BOOT-INF/classes中的业务类和application.yml被正确加载。BOOT-INF/lib中的 Tomcat、Spring Web 等依赖被隔离加载,无类路径冲突。- 内嵌 Tomcat 随 Spring 容器启动,应用可直接对外提供 HTTP 服务。
易错场景与面试考点
易错场景一:使用 maven-jar-plugin 代替 spring-boot-maven-plugin
<!-- 错误示范:忘记配置 spring-boot-maven-plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
</plugin>
后果:生成的 JAR 缺少 JarLauncher 和 BOOT-INF 结构,无法通过 java -jar 运行。小崔在测试环境部署时反复排查,最后发现 POM 中根本没有 Spring Boot 打包插件。
正确做法:始终使用 spring-boot-maven-plugin,并确保 repackage 目标被执行。继承 spring-boot-starter-parent 是最稳妥的方式。
易错场景二:生产 JAR 中误含 devtools
<!-- 错误示范:未排除 devtools -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
同时 pom.xml 的 <dependencies> 中保留了:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
后果:生产环境 JAR 中携带了 devtools,存在性能开销(额外监听文件变化)和潜在安全风险(远程触发重启漏洞)。
正确做法:插件配置中显式添加 <excludeDevtools>true</excludeDevtools>,或将 devtools 依赖的 <scope> 设为 runtime(虽然 Spring Boot 推荐 optional)。
面试考点
Q:Spring Boot 的 Fat JAR 和普通 JAR 有什么区别?
Fat JAR 通过
spring-boot-maven-plugin的repackage目标生成,内部包含BOOT-INF/classes(业务类)、BOOT-INF/lib(所有依赖 JAR)和JarLauncher启动器。普通 JAR 仅包含项目自身类,依赖需外部提供。
Q:JarLauncher 的作用是什么?
JarLauncher是 Spring Boot Loader 模块提供的入口类,由 JVM 标准类加载器加载。它负责创建LaunchedURLClassLoader,将BOOT-INF下的类和依赖 JAR 加入类路径,然后通过反射调用业务启动类的main方法。
Q:为什么 Spring Boot 不直接把业务启动类设为 Main-Class?
因为业务启动类及其依赖位于
BOOT-INF目录下,标准 JVM 类加载器无法直接识别该路径。需要JarLauncher先用标准类加载器加载,再由自定义类加载器接管后续类加载。
Q:如何查看 Fat JAR 内部的依赖列表?
使用
jar tf app.jar | grep BOOT-INF/lib可列出所有打包的依赖 JAR 文件名;或解压后通过BOOT-INF/classpath.idx(Spring Boot 2.3+)查看。
Q:repackage 目标的具体行为是什么?
repackage在maven-jar-plugin执行完毕后,将原始 JAR 重命名为.original,然后创建新的 Fat JAR,覆盖原始文件名。新 JAR 包含 Loader 类和BOOT-INF结构。