1 SpringBoot打Jar包运行原理
1.1 Spring boot的 jar可以直接运行
Spring Boot
的jar
是 Fat JAR
肥包, 不是 Thin JAR
。
-
Fat JAR
(如Spring Boot可执行JAR)则内嵌 所有 依赖 Jar 包,可直接运行。 -
Thin JAR
仅包含项目代码和 依赖Jar
包 的清单文件,依赖需额外配置;
Spring Boot
的 jar
是 Fat JAR
肥包 可以直接运行,已经包含了嵌入式Web服务器和打包了所有依赖项的可执行JAR
结构。
所以,Spring Boot
的jar 很大的,动不动 几百M。
Spring Boot的jar 是 Fat JAR 本质执行流程
java -jar app.jar
↓
JVM执行JarLauncher.main()
↓
加载BOOT-INF/lib中所有依赖
↓
启动嵌入式Tomcat(默认端口8080)
↓
执行@SpringBootApplication主类
Spring Boot的jar 是 Fat JAR 设计, 使Spring Boot应用具有"开箱即运行"的特性,简化了部署 的依赖管理。
1.2 Springboot Fat JAR目录结构
Springboot
jar 的 4个组成部分:
- 特殊JAR结构:使用
Spring Boot Maven/Gradle
插件打包时,会生成一个包含三个主要部分的fat JAR
:- 应用代码(在
BOOT-INF/classes
) - 所有依赖库(在
BOOT-INF/lib
) - Spring Boot启动加载器(在
org/springframework/boot/loader
)
- 应用代码(在
- 嵌入式服务器:
JAR内置了Tomcat/Jetty
等Web服务器,无需外部应用服务器 - 自定义类加载器:
通过JarLauncher
启动时,使用LaunchedURLClassLoader
加载依赖 -
MANIFEST.MF
配置:
指定了Main-Class
(org.springframework.boot.loader.JarLauncher
)和Start-Class
(主应用类)与普通JAR
(由maven-jar-plugin
生成)相比,Fat JAR
新增了两部分关键内容:-
BOOT-INF/lib
目录:存放项目所有Maven
依赖的 JAR 包(如 spring-boot-starter-web、jackson-databind 等)。 -
Spring Boot Loader
类:位于org.springframework.boot.loader
包下,包含自定义启动器和类加载器,解决嵌套 JAR 的加载问题。
-
spring boot fat jar目录结构如下:
spring boot fat jar和普通jar的区别如下:
比较维度 | 普通 JAR(thin JAR) | 标准 Fat Jar | Spring Boot Fat Jar |
---|---|---|---|
依赖包含方式 | 仅包含项目自身的编译代码和资源文件,不包含依赖库;运行时需手动配置类路径添加依赖 | 包含项目自身代码及所有依赖库(依赖字节码平铺至根目录),自包含运行 | 依赖库存储在 BOOT-INF/lib/ 目录下,通过自定义类加载器加载 |
可执行性 | 若无主类声明则不可直接执行;需通过 java -cp 指定主类和依赖路径 | 可声明主类(通过 MANIFEST.MF ),直接通过 java -jar 运行 |
内嵌 SpringApplication 启动类,支持 java -jar 直接运行 |
结构差异 | 标准结构:仅含 META-INF/ 和项目类文件目录 |
依赖与项目代码混合在根目录(平铺结构) | 含专属目录:BOOT-INF/classes/ (项目代码)、BOOT-INF/lib/ (依赖库) |
服务器支持 | 不包含服务器;Web应用需部署至外部服务器(如Tomcat) | 通常不包含服务器,需自行处理 | 内嵌服务器(如Tomcat/Jetty ),无需外部部署 |
适用场景 | 作为库文件供其他项目依赖 | 独立应用分发(简化依赖管理) | 微服务或独立Spring应用的一键部署 |
ClassPath生成逻辑 | 依赖路径由用户显式指定 | 依赖加载顺序由文件系统平铺结构决定 | 依赖按 BOOT-INF/lib/ 内JAR的Entry顺序加载 |
1.3 Jar 启动入口: MANIFEST.MF文件
MANIFEST.MF
文件 至关重要,是spring boot
的启动入口文件
在Spring Boot
的可执行Jar包(Fat Jar)中,META-INF
目录下的MANIFEST.MF
文件扮演着至关重要的角色。
MANIFEST.MF
文件包含了Jar
包的元数据,用于指导Java虚拟机(JVM)如何加载和运行Jar包。
以下是对MANIFEST.MF文件内容:
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: tms-start
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.sean.StartApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.4.5
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
关键属性:
-
Main-Class
:指向Spring Boot
内置的JarLauncher
,它是 JAR 执行的入口。 -
Start-Class
:指向应用主类(如 com.sean.StartApplication),由JarLauncher
反射调用其main
方法启动应用。 -
Spring-Boot-Classpath-Index
:类路径索引文件,优化启动速度 -
Spring-Boot-Layers-Index
:用于分层Docker
镜像构建
属性详细介绍:
-
Spring-Boot-Version: 2.4.5
,表示该Jar包是基于Spring Boot 2.4.5版本构建的。 -
Main-Class: org.springframework.boot.loader.JarLauncher
,指定了Jar包的入口类。当Jar包被运行时,JVM会首先执行这个类的main方法。在Spring Boot中,JarLauncher
负责加载和启动应用程序。 -
Start-Class: com.sean.StartApplication
,指定了Spring Boot
应用程序的实际入口类。JarLauncher
在启动时会使用反射调用这个类的main方法。 -
Spring-Boot-Classes: BOOT-INF/classes/
,指定了应用程序类文件的存放路径。这些类文件是由项目源代码编译生成的。 -
Spring-Boot-Lib: BOOT-INF/lib/
,指定了应用程序依赖的Jar包文件的存放路径。这些依赖是在构建过程中被复制到Jar包中的。 -
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
,(可选)用于优化类加载性能,通过索引文件来加速类路径的查找。 -
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
,(可选)在Spring Boot 2.3
及以上版本中引入,用于支持构建分层Jar包,以便在构建和运行时进行优化。
1.4 SpringBoot 如何打包
SpringBoot
是打包的 fat jar
可以直接运行,那spring boot
是如何打包的?
fat jar
和普通jar结构有什么区别?
要理解 Fat JAR
的生成过程,需先掌握 spring-boot-maven-plugin
这一核心插件的工作逻辑。作为 Maven
自定义插件,它通过绑定 Maven
生命周期的特定阶段(Phase
),实现对 JAR
包的重新打包(Repackage
)
1.4.1 Maven 生命周期与插件目标的绑定
Maven
拥有三套独立的生命周期(clean
、default
、site
),每个生命周期包含多个顺序执行的阶段(Phase
)
插件的目标(Goal
)需绑定到具体阶段,才能在构建过程中触发执行。
spring-boot-maven-plugin
通常绑定到 default
生命周期的 package
阶段,其核心目标是 repackage
(重新打包)。
配置示例如下:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 绑定 repackage 目标 -->
</goals>
</execution>
</executions>
</plugin>
1.4.2 repackage阶段核心逻辑
repackage
目标的执行入口是 org.springframework.boot.maven.RepackageMojo#execute
,其核心逻辑如下:
private void repackage() throws MojoExecutionException {
// 1. 获取原始 JAR(由 maven-jar-plugin 生成,最终重命名为 .original)
Artifact source = getSourceArtifact();
// 2. 定义最终输出的 Fat JAR 文件
File target = getTargetFile();
// 3. 创建 Repackager,负责实际打包逻辑
Repackager repackager = getRepackager(source.getFile());
// 4. 过滤项目运行时依赖(排除测试依赖等)
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
// 5. 将依赖转换为 Libraries 对象(统一管理依赖)
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());
try {
// 6. 生成启动脚本(可选)
LaunchScript launchScript = getLaunchScript();
// 7. 执行重新打包,生成 Fat JAR
repackager.repackage(target, libraries, launchScript);
} catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
// 8. 更新原始 JAR 为 .original 后缀
updateArtifact(source, target, repackager.getBackupFile());
}
1.4.3 Repackager 与文件布局(Layout)
Repackager
是实际执行打包的核心类,其初始化时会通过 layoutFactory
定义文件布局(即 JAR 内部目录结构)。
关键代码如下:
private Repackager getRepackager(File source) {
Repackager repackager = new Repackager(source, this.layoutFactory);
repackager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener());
//设置main class的名称,如果不指定的话则会查找第一个包含main方法的类,repacke最后将会设置org.springframework.boot.loader.JarLauncher
repackager.setMainClass(this.mainClass);
if (this.layout != null) {
getLog().info("Layout: " + this.layout);
//重点关心下layout 最终返回了 org.springframework.boot.loader.tools.Layouts.Jar
repackager.setLayout(this.layout.layout());
}
return repackager;
}
layout 定义了 JAR 的目录结构规范。
以 Layouts.Jar 为例(对应 Fat JAR):
/*
* Executable JAR layout.
*/
public static class Jar implements RepackagingLayout {
@Override
public String getLauncherClassName() {
return "org.springframework.boot.loader.JarLauncher"; // 启动类
}
@Override
public String getLibraryDestination(String libraryName, LibraryScope scope) {
return "BOOT-INF/lib/"; // 依赖 JAR 存放路径
}
@Override
public String getClassesLocation() {
return "";
}
@Override
public String getRepackagedClassesLocation() {
return "BOOT-INF/classes/"; // 应用类存放路径
}
@Override
public boolean isExecutable() {
return true;
}
}
2 类加载机制
2.1 JVM 默认的加载机制:双亲委派
2.2 SpringBoot 打破双亲
由于JVM
标准三层类加载器均不原生支持内嵌包加载,需通过自定义类加载器实现。 所以,Springboot
自定义了 自己的的 内嵌包加载器
LaunchedURLClassLoader
, 加载 FarJar
里边的内嵌包,而且,Springboot
需要打破双亲委派, 优先使用自己的内嵌包,而不是 Java类路径下的 公共包。
2.2.1 Springboot 优先使用自己的内嵌包
为啥 Springboot
需要 优先使用自己的内嵌包,而不是 Java类路径下的 公共包,有两个原因:
-
Java
类路径下的公共包
可能和Far
里边的内嵌包
存在 版本上的不同, 需要优先使用自己的版本
,而不是公共的版本。 - 不同的
SpringBoot
需要支持不同模块或插件加载相同类的不同版本 , 优先加载内嵌包
, 可独立管理依赖,实现版本隔离, 以及实现模块化隔离
,但是Fat JAR
包含嵌套的依赖JAR(如 BOOT-INF/lib/*.jar)
,而Java
原生类加载器无法直接加载嵌套JAR
SpringBoot
通过自定义类加载器和扩展 JAR 协议解决了这一问题。
Springboot loader
目录下的 自定义启动器和类加载器 org.springframework.boot.loader.JarLauncher
,实现了下面两个功能:
- 解决
内嵌 JAR包
的加载问题 - 打破双亲委派模式
因为spring boot jar
打包含专属目录:BOOT-INF/classes/
(项目代码)、BOOT-INF/lib/
(依赖库),假设系统采用Spring Boot打包:
express-locker.jar
├── BOOT-INF/
│ ├── classes/ (主程序)
│ └── lib/
│ ├── sms-service.jar (短信服务)
│ └── payment.jar (支付模块)
当主程序需要加载sms-service.jar
里的短信模板,或者某个类时,这时资源(类)访问地址大致如下:URL url = new URL("jar:file:express-locker.jar!/BOOT-INF/lib/sms-service.jar!/templates/alert.txt");
这里就需要打破双亲委派,同时要能对这种自定义协议的url
进行嵌套加载
2.2.2 核心源码:LaunchedURLClassLoader 打破双亲委派
SpringBoot
的LaunchedURLClassLoader
通过重写loadClass
方法打破双亲委派机制,优先加载BOOT-INF/
下的嵌套JAR资源。
其加载顺序为:
- 当前加载器扫描嵌套jar;
- 失败后委托父加载器。
这种设计实现了独立可执行JAR
的模块化加载,解决了传统委派模式无法访问嵌套依赖的问题,是Spring Boot Fat Jar
运行的核心机制。
public class LaunchedURLClassLoader extends URLClassLoader {
// 特殊处理的包前缀
private static final String[] DEFAULT_HIDDEN_PACKAGES = new String[] {
"java.", "javax.", "jakarta.", "org.springframework.boot.loader."
};
// 覆盖的类加载逻辑
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1、 检查是否已加载
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass != null) {
return loadedClass;
}
// 2. 检查是否在隐藏包中
if (isHidden(name)) {
return super.loadClass(name, resolve);
}
// 3. 尝试从BOOT-INF/classes加载
try {
Class<?> localClass = findClass(name);
if (localClass != null) {
return localClass;
}
} catch (ClassNotFoundException ex) {}
// 4. 尝试从父加载器加载
try {
return Class.forName(name, false, getParent());
} catch (ClassNotFoundException ex) {}
// 5. 最后尝试从BOOT-INF/lib加载
return findClass(name);
}
}
private boolean isHidden(String className) {
for (String hiddenPackage : DEFAULT_HIDDEN_PACKAGES) {
if (className.startsWith(hiddenPackage)) {
return true;
}
}
return false;
}
}
2.2.3 JarURLConnection 嵌套 Jar 资源加载
SpringBoot
的JarURLConnection
通过扩展URLConnection
实现嵌套JAR资源加载,核心机制包括:
- 自定义协议处理器解析
jar:file:/xxx.jar!/BOOT-INF/lib/nested.jar!/
格式路径; - 使用
JarFile
和JarEntry
逐层解包嵌套JAR; - 配合
LaunchedURLClassLoader
实现资源定位。
该设计解决了传统URL协议无法访问多层嵌套JAR的问题,是FatJar运行的基础支撑。
public class JarURLConnection extends java.net.JarURLConnection {
private JarFile jarFile;
public InputStream getInputStream() throws IOException {
// 处理嵌套jar的路径格式:jar:nested:/path.jar!/nested.jar!/
String nestedPath = getEntryName();
if (nestedPath.contains("!/")) {
String[] parts = nestedPath.split("!/", 2);
JarFile outerJar = new JarFile(parts[0]);
JarEntry nestedEntry = outerJar.getJarEntry(parts[1]);
return new NestedJarInputStream(outerJar, nestedEntry);
}
return super.getInputStream();
}
}
2.2.4 JarLauncher 启动过程
以下是 Spring Boot JarLauncher
启动的大致步骤:
- JVM入口调用
执行java -jar
命令触发MANIFEST.MF
中指定的Main-Class: org.springframework.boot.loader.JarLauncher
,初始化JarLauncher
实例并调用其main()
方法 - 类加载器构建
- 创建
LaunchedURLClassLoader
,优先加载BOOT-INF/classes
和BOOT-INF/lib/*
下的资源 - 注册自定义
JarURL
协议处理器,支持嵌套JAR路径解析(如jar:file:/app.jar!/BOOT-INF/lib/nested.jar!/
)
- 创建
- 反射启动应用
- 通过
MANIFEST.MF
的Start-Class
定位用户主程序(如com.example.Application) - 反射调用用户类的main()方法,移交控制权至SpringApplication启动流程
- 通过
关键步骤时序:
JVM
→ JarLauncher.main()
→ LaunchedURLClassLoader
加载 → 反射调用Start-Class
→ SpringApplication.run()
该流程通过打破双亲委派实现嵌套JAR
资源加载,确保FatJar
自包含运行
执行java -jar
时,JVM读取MANIFEST.MF
中的Main-Class
(通常是org.springframework.boot.loader.JarLauncher
)
JarLauncher 核心代码:
public class JarLauncher extends ExecutableArchiveLauncher {
@Override
protected String getMainClass() throws Exception {
// 从 MANIFEST.MF 读取 Start-Class
return getArchive().getManifest().getMainAttributes()
.getValue("Start-Class");
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
2.2.5 Spring Boot应用 启动过程 总结
-
java -jar
启动过程中,会首先Paths类找到对应的启动jar的位置信息。 - 读取
mainfest.mf
文件里面的Main-class
,启动JarLauncher
- 使用
LaunchedURLClassLoader
类加载器,完成对当前类依赖的加载,如果当前类不存在,则去super(打破了双亲委派)。 -
LaunchedURLClassLoader
类加载器通过使用JarURLConnection
解决嵌套 JAR 的加载问题 - 加载主类
start-class
,并且执行main方法。
这种设计使得Spring Boot应用可以像普通可执行文件一样运行,简化了部署和分发过程。