365网站买球违法吗-365客服电话-365世界杯

哈罗二面:为什么Spring boot的 jar 可以直接运行?

哈罗二面:为什么Spring boot的 jar 可以直接运行?

本文 的 原文 地址

本文 的 原文 地址

哈罗二面:为什么Spring boot的 jar 可以直接运行?

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的相关面试题:

为什么Spring boot的 jar 可以直接运行?

Spring boot的 fat jar是 如何 打破双亲委派, 完成 内嵌包 的加载的?

最近有小伙伴在面试 哈罗,是按照 尼恩的套路去做答的, 拿到了大厂offer, 欢天喜地。

所以,尼恩给小伙的这个大厂面试题,大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。

最终,机会爆表,实现”offer自由” 。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V175版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取

本文作者:

第一作者 老架构师 肖恩(肖恩 是尼恩团队 高级架构师,负责写此文的第一稿,初稿 )

第二作者 老架构师 尼恩 (45岁老架构师, 负责提升此文的 技术高度,让大家有一种 俯视 技术、 技术自由 的感觉)

为什么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应用具有"开箱即运行"的特性,简化了部署 的依赖管理。

普通 的Fat JAR的结构

当然,要理解spring boot jar为什么可以直接运行,就要清楚fat jar的结构,以及很普通jar的区别

可执行 JAR(Fat JAR)是一种自包含的一体化 Fat JAR 包。

普通 Fat JAR 核心特点是将应用代码、所有第三方依赖(如 Spring 框架、数据库驱动等) 打包为一个文件,无需额外依赖即可通过 java -jar 命令直接运行。

Springboot Fat JAR目录结构

spring boot jar 的 4个组成部分

1、 特殊JAR结构:使用Spring Boot Maven/Gradle插件打包时,会生成一个包含三个主要部分的"fat JAR":

应用代码(在BOOT-INF/classes)

所有依赖库(在BOOT-INF/lib)

Spring Boot启动加载器(在org/springframework/boot/loader)

2、 嵌入式服务器:

JAR内置了Tomcat/Jetty等Web服务器,无需外部应用服务器

3、 自定义类加载器:

通过JarLauncher启动时,使用LaunchedURLClassLoader加载依赖

4、 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顺序加载

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包,以便在构建和运行时进行优化。

类加载机制

JVM 默认的加载机制:双亲委派。

双亲委派是 自顶向下加载。

JDK的类加载器分为启动类加载器(Bootstrap)、扩展类加载器(Extension)和应用类加载器(Application)三层,开发者也可自定义类加载器。

Java类加载机制的核心原则,其工作原理是:当一个类加载器收到加载请求时,会先将请求委派给父加载器处理,只有父加载器无法完成加载时,子加载器才会尝试加载。

这种层次化的委派机制(自底向上检查,自顶向下加载)保证了Java核心类库的安全性(如防止用户伪造java.lang.Object类),同时避免了类的重复加载。

Java类加载器的三层结构及其加载路径如下:

启动类加载器(Bootstrap ClassLoader)‌

‌加载路径‌:JAVA_HOME/jre/lib目录下的核心类库,如rt.jar、resources.jar、charsets.jar等 。

‌扩展类加载器(Extension ClassLoader)‌

加载路径‌:JAVA_HOME/jre/lib/ext目录下的JAR文件。系统属性java.ext.dirs指定的目录 。

‌特点‌:由Java实现(sun.misc.Launcher$ExtClassLoader),用于加载Java扩展库 。

‌应用类加载器(Application ClassLoader)‌

‌加载路径‌:用户类路径(Classpath),包括环境变量CLASSPATH或-classpath参数指定的路径。

标准三层类加载器均‌不原生支持‌内嵌包加载,需通过自定义类加载器实现。 所以, Springboot自定义了 自己的的 ‌内嵌包加载器 LaunchedURLClassLoader , 加载 FarJar 里边的内嵌包,而且,Springboot 需要打破双亲委派, 优先使用自己的内嵌包,而不是 Java类路径下的 公共包。

为啥 Springboot 需要 优先使用自己的内嵌包?

为啥 Springboot 需要 优先使用自己的内嵌包,而不是 Java类路径下的 公共包?

两个原因:

Java类路径下的 公共包 可能和 Far 里边的 内嵌包 存在 版本上的不同, 需要优先使用 自己的版本,而不是公共的版本。

不同的 Spring Boot 需要支持不同模块或插件加载相同类的不同版本 , 优先加载 内嵌包, 可独立管理依赖,实现版本隔离, 以及实现 模块化隔离

但是 Fat JAR 包含嵌套的依赖 JAR(如 BOOT-INF/lib/*.jar),而 Java 原生类加载器无法直接加载嵌套 JAR。

Spring Boot 通过自定义类加载器和扩展 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进行嵌套加载

核心源码:LaunchedURLClassLoader 打破双亲委派

Spring Boot的LaunchedURLClassLoader通过重写loadClass方法打破双亲委派机制,优先加载BOOT-INF/下的嵌套JAR资源。

其加载顺序为:

1、 当前加载器扫描嵌套jar;

2、 失败后委托父加载器。

这种设计实现了独立可执行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;

}

}

JarURLConnection 嵌套 Jar 资源加载

Spring Boot的JarURLConnection通过扩展URLConnection实现嵌套JAR资源加载,核心机制包括:

1、 自定义协议处理器解析jar:file:/xxx.jar!/BOOT-INF/lib/nested.jar!/格式路径;

2、 使用JarFile和JarEntry逐层解包嵌套JAR;

3、 配合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();

}

}

JarLauncher 启动过程

以下是Spring Boot JarLauncher启动的大致步骤:

(1)JVM入口调用‌

执行java -jar命令触发MANIFEST.MF中指定的Main-Class: org.springframework.boot.loader.JarLauncher

初始化JarLauncher实例并调用其main()方法

(2)类加载器构建‌

创建LaunchedURLClassLoader,优先加载BOOT-INF/classes和BOOT-INF/lib/*下的资源

注册自定义JarURL协议处理器,支持嵌套JAR路径解析(如jar:file:/app.jar!/BOOT-INF/lib/nested.jar!/)

‌(3)反射启动应用‌

通过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);

}

}

JarLauncher启动后,会产生设置特殊的类加载器。

类加载器实现 (LaunchedURLClassLoader)代码示例:

public class LaunchedURLClassLoader extends URLClassLoader {

public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {

super(urls, parent);

}

@Override

protected Class loadClass(String name, boolean resolve)

throws ClassNotFoundException {

// 特殊处理嵌套 Jar 的类加载

if (name.startsWith("org.springframework.boot.loader")) {

//调整了传统的双亲委派模型),允许优先从当前 JAR 中加载类,而不是完全依赖父级类加载器。

return super.loadClass(name, resolve);

}

// 优先从 BOOT-INF/classes 加载

try {

return findClass(name);

} catch (ClassNotFoundException ex) {

// 然后从 BOOT-INF/lib 加载

return super.loadClass(name, resolve);

}

}

}

加载并执行BOOT-INF/classes中真正的应用主类(由MANIFEST.MF中的Start-Class指定)

Spring Boot应用由此启动

Spring Boot应用 启动过程 总结:

1、java -jar启动过程中,会首先Paths类找到对应的启动jar的位置信息。

2、读取mainfest.mf文件里面的Main-class,启动JarLauncher

3、使用LaunchedURLClassLoader类加载器,完成对当前类依赖的加载,如果当前类不存在,则去super(打破了双亲委派)。

4、LaunchedURLClassLoader类加载器通过使用JarURLConnection解决嵌套 JAR 的加载问题

4、加载主类start-class,并且执行main方法。

这种设计使得Spring Boot应用可以像普通可执行文件一样运行,简化了部署和分发过程。

spring boot 是如何打包的?

spring boot 是打包的fat jar可以直接运行,那spring boot是如何打包的?

fat jar和普通jar结构有什么区别?

要理解 Fat JAR 的生成过程,需先掌握 spring-boot-maven-plugin 这一核心插件的工作逻辑。

作为 Maven 自定义插件,它通过绑定 Maven 生命周期的特定阶段(Phase),实现对 JAR 包的重新打包(Repackage)。

Maven 生命周期与插件目标的绑定

Maven 拥有三套独立的生命周期(clean、default、site),每个生命周期包含多个顺序执行的阶段(Phase)。

插件的目标(Goal)需绑定到具体阶段,才能在构建过程中触发执行。

spring-boot-maven-plugin 通常绑定到 default 生命周期的 package 阶段,其核心目标是 repackage(重新打包)。

配置示例如下:

org.springframework.boot

spring-boot-maven-plugin

repackage

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 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());

}

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;

}

}

遇到问题,找老架构师取经

借助此文的问题 套路 ,大家可以 放手一试,保证 offer直接到手,还有可能会 涨薪 100%-200%。

后面,尼恩java面试宝典回录成视频, 给大家打造一套进大厂的塔尖视频。

在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。

很多小伙伴刷完后, 吊打面试官, 大厂横着走。

在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。

遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。

尼恩指导了大量的小伙伴上岸,前段时间,刚指导 32岁 高中生,冲大厂成功。特批 成为 架构师,年薪 50W,逆天改命 !!!。

狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。

会 AI的程序员,工资暴涨50%!