网易首页 > 网易号 > 正文 申请入驻

【324期】一文彻底搞懂 SpringBoot jar 可执行原理!

0
分享至

文章篇幅较长,但是包含了SpringBoot 可执行jar包从头到尾的原理,请读者耐心观看。

涉及的知识点主要包括Maven的生命周期以及自定义插件,JDK提供关于jar包的工具类以及Springboot如何扩展,最后是自定义类加载器。

SpringBoot 的可执行jar包又称fat jar ,是包含所有第三方依赖的 jar 包,jar 包中嵌入了除 java 虚拟机以外的所有依赖,是一个all-in-one jar包。

普通插件maven-jar-plugin生成的包和spring-boot-maven-plugin生成的包之间的直接区别,是fat jar中主要增加了两部分,第一部分是lib目录,存放的是Maven依赖的jar包文件,第二部分是spring boot loader相关的类。

fat jar //目录结构
├─BOOT-INF
│ ├─classes
│ └─lib
├─META-INF
│ ├─maven
│ ├─app.properties
│ ├─MANIFEST.MF
└─org
└─springframework
└─boot
└─loader
├─archive
├─data
├─jar
└─util

也就是说想要知道fat jar是如何生成的,就必须知道spring-boot-maven-plugin工作机制,而spring-boot-maven-plugin属于自定义插件,因此我们又必须知道,Maven的自定义插件是如何工作的

Maven 拥有三套相互独立的生命周期: clean、default 和 site, 而每个生命周期包含一些phase阶段, 阶段是有顺序的, 并且后面的阶段依赖于前面的阶段。生命周期的阶段phase与插件的目标goal相互绑定,用以完成实际的构建任务。


org.springframework.bootgroupId>
spring-boot-maven-pluginartifactId>
repackagegoal>
goals>
execution>
executions>
plugin>

repackage目标对应的将执行到org.springframework.boot.maven.RepackageMojo#execute,该方法的主要逻辑是调用了org.springframework.boot.maven.RepackageMojo#repackage

private void repackage() throws MojoExecutionException {
//获取使用maven-jar-plugin生成的jar,最终的命名将加上.orignal后缀
Artifact source = getSourceArtifact();
//最终文件,即Fat jar
File target = getTargetFile();
//获取重新打包器,将重新打包成可执行jar文件
Repackager repackager = getRepackager(source.getFile());
//查找并过滤项目运行时依赖的jar
Set artifacts = filterDependencies(this.project.getArtifacts(),
getFilters(getAdditionalFilters()));
//将artifacts转换成libraries
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
getLog());
try {
//提供Spring Boot启动脚本
LaunchScript launchScript = getLaunchScript();
//执行重新打包逻辑,生成最后fat jar
repackager.repackage(target, libraries, launchScript);
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
//将source更新成 xxx.jar.orignal文件
updateArtifact(source, target, repackager.getBackupFile());

我们关心一下org.springframework.boot.maven.RepackageMojo#getRepackager这个方法,知道Repackager是如何生成的,也就大致能够推测出内在的打包逻辑。

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;

* 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/";
@Override
public String getClassesLocation() {
return "";
@Override
public String getRepackagedClassesLocation() {
return "BOOT-INF/classes/";
@Override
public boolean isExecutable() {
return true;

layout我们可以将之翻译为文件布局,或者目录布局,代码一看清晰明了,同时我们需要关注,也是下一个重点关注对象org.springframework.boot.loader.JarLauncher,从名字推断,这很可能是返回可执行jar文件的启动类。

Manifest-Version: 1.0
Implementation-Title: oneday-auth-server
Implementation-Version: 1.0.0-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: oneday
Implementation-Vendor-Id: com.oneday
Spring-Boot-Version: 2.1.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.oneday.auth.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_171

repackager生成的MANIFEST.MF文件为以上信息,可以看到两个关键信息Main-ClassStart-Class。我们可以进一步,程序的启动入口并不是我们SpringBoot中定义的main,而是JarLauncher#main,而再在其中利用反射调用定义好的Start-Class的main方法

重点类介绍


  • java.util.jar.JarFile JDK工具类提供的读取jar文件



  • org.springframework.boot.loader.jar.JarFileSpringboot-loader继承JDK提供JarFile类



  • java.util.jar.JarEntryDK工具类提供的jar文件条目



  • org.springframework.boot.loader.jar.JarEntry Springboot-loader继承JDK提供JarEntry类



  • org.springframework.boot.loader.archive.ArchiveSpringboot抽象出来的统一访问资源的层


    • JarFileArchivejar包文件的抽象

    • ExplodedArchive文件目录

这里重点描述一下JarFile的作用,每个JarFileArchive都会对应一个JarFile。在构造的时候会解析内部结构,去获取jar包里的各个文件或文件夹类。我们可以看一下该类的注释。

/* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality.


  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
    * on any directory entry.

  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
    * embedded JAR files (as long as their entry is not compressed).


jar里的资源分隔符是!/,在JDK提供的JarFile URL只支持一个’!/‘,而Spring boot扩展了这个协议,让它支持多个’!/‘,就可以表示jar in jar、jar in directory、fat jar的资源了。

自定义类加载机制


  • 最基础:Bootstrap ClassLoader(加载JDK的/lib目录下的类)



  • 次基础:Extension ClassLoader(加载JDK的/lib/ext目录下的类)



  • 普通:Application ClassLoader(程序自己classpath下的类)



  • 另外,更多关于SpringBoot面试题,公众号Java精选,回复java面试,获取面试资料。


首先需要关注双亲委派机制很重要的一点是,如果一个类可以被委派最基础的ClassLoader加载,就不能让高层的ClassLoader加载,这样是为了范围错误的引入了非JDK下但是类名一样的类。

其二,如果在这个机制下,由于fat jar中依赖的各个第三方jar文件,并不在程序自己classpath下,也就是说,如果我们采用双亲委派机制的话,根本获取不到我们所依赖的jar包,因此我们需要修改双亲委派机制的查找class的方法,自定义类加载机制。

先简单的介绍Springboot2中LaunchedURLClassLoader,该类继承了java.net.URLClassLoader,重写了java.lang.ClassLoader#loadClass(java.lang.String, boolean),然后我们再探讨他是如何修改双亲委派机制。

在上面我们讲到Spring boot支持多个’!/‘以表示多个jar,而我们的问题在于,如何解决查找到这多个jar包。我们看一下LaunchedURLClassLoader的构造方法。

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

urls注释解释道the URLs from which to load classes and resources,即fat jar包依赖的所有类和资源,将该urls参数传递给父类java.net.URLClassLoader,由父类的java.net.URLClassLoader#findClass执行查找类方法,该类的查找来源即构造方法传递进来的urls参数。

//LaunchedURLClassLoader的实现
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Handler.setUseFastConnectionExceptions(true);
try {
try {
//尝试根据类名去定义类所在的包,即java.lang.Package,确保jar in jar里匹配的manifest能够和关联 //的package关联起来
definePackageIfNecessary(name);
catch (IllegalArgumentException ex) {
// Tolerate race condition due to being parallel capable
if (getPackage(name) == null) {
// This should never happen as the IllegalArgumentException indicates
// that the package has already been defined and, therefore,
// getPackage(name) should not return null.

//这里异常表明,definePackageIfNecessary方法的作用实际上是预先过滤掉查找不到的包
throw new AssertionError("Package " + name + " has already been "
+ "defined but it could not be found");
}
}
return super.loadClass(name, resolve);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}

方法super.loadClass(name, resolve)实际上会回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循双亲委派机制进行查找类,而Bootstrap ClassLoader和Extension ClassLoader将会查找不到fat jar依赖的类,最终会来到Application ClassLoader,调用java.net.URLClassLoader#findClass

如何真正的启动

Springboot2和Springboot1的最大区别在于,Springboo1会新起一个线程,来执行相应的反射调用逻辑,而SpringBoot2则去掉了构建新的线程这一步。

方法是org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)反射调用逻辑比较简单,这里就不再分析,比较关键的一点是,在调用main方法之前,将当前线程的上下文类加载器设置成LaunchedURLClassLoader

protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
Demopublic static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
JarFile.registerUrlProtocolHandler();
// 构造LaunchedURLClassLoader类加载器,这里使用了2个URL,分别对应jar包中依赖包spring-boot-loader和spring-boot,使用 "!/" 分开,需要org.springframework.boot.loader.jar.Handler处理器处理
LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(
new URL[] {
new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-loader-1.2.3.RELEASE.jar!/")
, new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-2.1.3.RELEASE.jar!/")
},
Application.class.getClassLoader());
// 加载类
// 这2个类都会在第二步本地查找中被找出(URLClassLoader的findClass方法)
classLoader.loadClass("org.springframework.boot.loader.JarLauncher");
classLoader.loadClass("org.springframework.boot.SpringApplication");
// 在第三步使用默认的加载顺序在ApplicationClassLoader中被找出
classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");

// SpringApplication.run(Application.class, args);
}

org.springframework.bootgroupId>
spring-boot-loaderartifactId>
2.1.3.RELEASEversion>
dependency>
org.springframework.bootgroupId>
spring-boot-maven-pluginartifactId>
2.1.3.RELEASEversion>

dependency>
总结

对于源码分析,这次的较大收获则是不能一下子去追求弄懂源码中的每一步代码的逻辑,即便我知道该方法的作用。我们需要搞懂的是关键代码,以及涉及到的知识点。

我从Maven的自定义插件开始进行追踪,巩固了对Maven的知识点,在这个过程中甚至了解到JDK对jar的读取是有提供对应的工具类。最后最重要的知识点则是自定义类加载器。整个代码下来并不是说代码究竟有多优秀,而是要学习他因何而优秀。

作者:plz叫我红领巾 https://juejin.cn/post/6844903890555502606

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

最近有很多人问,有没有读者交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群!

(微信小程序):3000+道面试题,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计等,在线随时刷题!

------ 特别推荐 ------

特别推荐:专注分享最前沿的技术与资讯,为弯道超车做好准备及各种开源项目与高效率软件的公众号,「大咖笔记」,专注挖掘好东西,非常值得大家关注。点击下方公众号卡片关注

文章有帮助的话,在看,转发吧!

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
去医院开药,医生说“这个不报销”,你可以反问一句话!

去医院开药,医生说“这个不报销”,你可以反问一句话!

小谈食刻美食
2026-06-02 07:24:18
9年亡命作恶108起、残杀13人、施暴99人!1962-2002云贵悍匪始末

9年亡命作恶108起、残杀13人、施暴99人!1962-2002云贵悍匪始末

墨策史
2026-05-31 00:40:08
美若天仙王楚然:肤白貌美,婀娜多姿。倾国倾城,美不胜收!

美若天仙王楚然:肤白貌美,婀娜多姿。倾国倾城,美不胜收!

十为先生
2026-05-09 15:19:03
提前剧透?CBA总决赛G4还未开赛,维基百科已显示上海夺冠

提前剧透?CBA总决赛G4还未开赛,维基百科已显示上海夺冠

懂球帝
2026-06-02 12:06:24
挂断特朗普电话后,沙特王储放下狠话:我们永远不会当美国的筹码

挂断特朗普电话后,沙特王储放下狠话:我们永远不会当美国的筹码

牛锅巴小钒
2026-06-02 13:46:09
与恩师穆里尼奥重聚皇马?德布劳内吐槽孔蒂,映射阿囧足球也乏味

与恩师穆里尼奥重聚皇马?德布劳内吐槽孔蒂,映射阿囧足球也乏味

穆里尼奥主义者
2026-06-01 21:21:25
币安上线8000只美股交易 稳定币直接买零佣金

币安上线8000只美股交易 稳定币直接买零佣金

全栈遛狗员
2026-06-01 18:39:23
688079,3分钟20%涨停!

688079,3分钟20%涨停!

证券时报
2026-06-02 11:03:01
CBA第一小外援要走?广东有望抢下“得分机器”,保底能进总决赛

CBA第一小外援要走?广东有望抢下“得分机器”,保底能进总决赛

绯雨儿
2026-06-02 10:07:47
牺牲太大!神十六返回,航天员出舱后为何脸部浮肿,被抬着走?

牺牲太大!神十六返回,航天员出舱后为何脸部浮肿,被抬着走?

大运河时空
2026-04-22 08:50:03
不能二次加热的6种食物!医生提醒:吃不完或倒掉,别乱节俭

不能二次加热的6种食物!医生提醒:吃不完或倒掉,别乱节俭

冷眼看世界728
2026-05-12 20:46:26
记一次“约炮”被骗的详细经过

记一次“约炮”被骗的详细经过

云上南安
2026-04-06 17:11:46
从14万到5.5万,蒸发2500万:广州天河顶豪,戳破了豪宅的旧神话

从14万到5.5万,蒸发2500万:广州天河顶豪,戳破了豪宅的旧神话

地产一品塘
2026-06-02 08:00:16
不听大陆劝告执意访美,郑丽文人未启程,就遭美方公开敲打!

不听大陆劝告执意访美,郑丽文人未启程,就遭美方公开敲打!

坠入二次元的海洋
2026-06-01 21:27:00
为什么去过朝鲜回来就沉默的人,不是隐瞒,是真的说不出

为什么去过朝鲜回来就沉默的人,不是隐瞒,是真的说不出

老特有话说
2026-05-12 15:41:08
钱再多有什么用?杜淳老婆自曝切了一片肺,给所有人狠狠上了一课

钱再多有什么用?杜淳老婆自曝切了一片肺,给所有人狠狠上了一课

聊历史的阿稼
2026-06-02 12:17:50
禁投美股,难得全球大国中惟一正确

禁投美股,难得全球大国中惟一正确

家传编辑部
2026-05-30 12:24:47
摧毁一架特别大飞机,击俄无线电中心!俄罗斯被打得柴油产量锐减

摧毁一架特别大飞机,击俄无线电中心!俄罗斯被打得柴油产量锐减

鹰眼Defence
2026-05-31 16:53:51
总决赛情报员!索汉支招尼克斯防守前队友文班:他很容易体能透支

总决赛情报员!索汉支招尼克斯防守前队友文班:他很容易体能透支

罗说NBA
2026-06-02 07:10:43
轮到以色列被炸!内塔的最大麻烦曝光,不是伊朗不是特朗普,是谁

轮到以色列被炸!内塔的最大麻烦曝光,不是伊朗不是特朗普,是谁

离离言几许
2026-06-02 13:49:38
2026-06-02 15:11:00
Java精选
Java精选
一场永远也演不完的戏
1792文章数 3859关注度
往期回顾 全部

科技要闻

烧掉千亿后,美团、阿里、京东谁先止血?

头条要闻

上海女童几乎没上过学 外公找来前女婿把女儿告上法庭

头条要闻

上海女童几乎没上过学 外公找来前女婿把女儿告上法庭

体育要闻

1米74的业余联赛替补,在英超踢中卫

娱乐要闻

奚梦瑶何猷君婚礼曝光 深情热吻甜蜜

财经要闻

锂电“资源墙”高筑 全球性长期博弈开始

汽车要闻

星途神秘新车轮廓曝光 又一款性能SUV要来了?

态度原创

艺术
旅游
房产
家居
本地

艺术要闻

周杰伦花 1.36 亿拍下这幅画

旅游要闻

河南千年宋陵开镰收麦,石像生与金色麦浪跨越时空同框,大批游客前来体验人工收割,网友调侃“人比麦子还多” ,文物局提醒

房产要闻

100亿!1371亩!海口城市更新,再爆超级项目!

家居要闻

流线型轮廓 包容多元身形

本地新闻

用剪纸的方式,打开江苏扬州

无障碍浏览 进入关怀版