百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

从原理和源码梳理Springboot FatJar 的机制

csdh11 2025-03-03 18:11 1 浏览

一、概述

SpringBoot FatJar 的设计,打破了标准 jar 的结构,在 jar 包内携带了其所依赖的 jar 包,通过 jar 中的 main 方法创建自己的类加载器,来识别加载运行其不规范的目录下的代码和依赖。

二、标准的 jar 包结构

打开 Java 的 Jar 文件我们经常可以看到文件中包含着一个META-INF目录,这个目录下会有一些文件,其中必有一个MANIFEST.MF,这个文件描述了该 Jar 文件的很多信息 其中 Main-Class 定义 Jar 文件的入口类,该类必须是一个可执行的类,一旦定义了该属性即可通过 java -jar xxx.jar 来运行该 jar 文件。

在生产环境是使用 java -jar xxx.jar 的方式来运行 SpringBoot 程序。 这种情况下,SpringBoot 应用真实的启动类并不是我们所定义的带有 main 方法的类,而是 JarLauncher 类。查看 SpringBoot 所打成的 FatJar,其 Main-Class 是
org.springframework.boot.loader.JarLauncher,这便是微妙之处。

Spring-Boot-Version: 2.1.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.rock.springbootlearn.SpringbootLearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk: 1.8.0_131
复制代码

JAR 包中的 MANIFEST.MF 文件详解以及编写规范

三、探索JarLauncher


org.springframework.boot.loader.JarLauncher这个类是哪里来的呢?答案在 spring-boot-loader-***.jar 包中,可找到这个 JarLauncher 类的源码。在项目中加入 maven 依赖,以便查看源码和远程调试。


    org.springframework.boot
    spring-boot-loader

复制代码

认真比较可以看出,这个 spring-boot-loader 包中的内容与 SpringBoot 的 FatJar 包中的一部分内容几乎一样。JarLauncher 在 jar 中的位置如下:

3.1 只能拷贝出来一份儿

重点重点重点:因 jar 规范要求 Main-Class 所指定的类必须位于 jar 包的顶层目录下,即
org.springframework.boot.loader.JarLauncher 这个 org 必须位于 jar 包中的第一级目录,不能放置在其他的目录下。所以所以所以只能将 spring-boot-loader 这个 jar 包的内容拷贝出来,而不是整个 jar 直接放置于执行 Jar 中。

3.2 携带程序所依赖的jar而非仅class

上边 JarLauncher 的这个 org.springframework.xx 以及 META-INF 这两个目录是符合 jar 包规范的。但是 BOOT-INF 这个目录里边有点像我们开发中的一些用法:

  • 依赖 jar 包在 lib 目录下 但按照 jar 包规范 jar 中不能有 jar 包的情况下
  • 程序.class 文件在 classes 目录下 但xxx.class 文件应该按照 org.springframework.xx 这样放置在 jar 中的根目录中

所以classes 和 lib 你也能意识到,这个设计是独特的。早期 jar 包内携带依赖是采用如 maven-shade-plugin 的做法,把依赖的class文件拷贝到目标 jar 中,但也会造成重名(全限定名)的类会出现覆盖的情况。后来 SpringBoot 为了避免覆盖的情况,修改了打包机制,放弃了maven-shade-plugin那种拷贝class的方式,调整为依赖原始 jar 包;这同时意味着改变了 Jar 标准的运行机制,那么要想让classes和lib中代码能够正常运行,你试想一下如果没有自定义的 classLoader 来加载这些类文件,可以嘛?

四、 自定义类加载器的运行机制

自定义类加载器的常规处理:

  1. 指定资源
  2. 指定委托关系
  3. 指定线程上下文类加载器
  4. 调用逻辑入口方法

4.1 指定资源

构造方法中基于 jar 包的文件系统信息,构造 Archive 对象

public ExecutableArchiveLauncher() {
	this.archive = createArchive();
}

protected final Archive createArchive() throws Exception {
	ProtectionDomain protectionDomain = getClass().getProtectionDomain();
	CodeSource codeSource = protectionDomain.getCodeSource();
	URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
	String path = (location != null) ? location.getSchemeSpecificPart() : null;
	if (path == null) {
		throw new IllegalStateException("Unable to determine code source archive");
	}
	File root = new File(path);
	if (!root.exists()) {
		throw new IllegalStateException(
				"Unable to determine code source archive from " + root);
	}
	return (root.isDirectory() ? new ExplodedArchive(root)
			: new JarFileArchive(root));
}
复制代码

采集 jar 包中的 classes 和 lib 目录下的归档文件。后边创建 ClassLoader 的时候作为参数传入

@Override
protected List getClassPathArchives() throws Exception {
	List archives = new ArrayList<>(
			this.archive.getNestedArchives(this::isNestedArchive));
	postProcessClassPathArchives(archives);
	return archives;
}

protected boolean isNestedArchive(Archive.Entry entry) {
	if (entry.isDirectory()) {
		return entry.getName().equals(BOOT_INF_CLASSES);
	}
	return entry.getName().startsWith(BOOT_INF_LIB);
}
复制代码
public static void main(String[] args) throws Exception {
	new JarLauncher().launch(args);
}
复制代码

4.2 创建自定义 ClassLoader

protected void launch(String[] args) throws Exception {
	JarFile.registerUrlProtocolHandler();
        //创建类加载器, 并指定归档文件
	ClassLoader classLoader = createClassLoader(getClassPathArchives());
	launch(args, getMainClass(), classLoader);
}
//创建类加载器, 将归档文件转换为URL
protected ClassLoader createClassLoader(List archives) throws Exception {
	List urls = new ArrayList<>(archives.size());
	for (Archive archive : archives) {
		urls.add(archive.getUrl());
	}
	return createClassLoader(urls.toArray(new URL[0]));
}
//父加载器是AppClassLoader
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        //getClass().getClassLoader() 是系统类加载器,因为默认情况下main方法所在类是由SystemClassLoader加载的,默认情况下是AppClassLoader.
	return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

复制代码

4.3 设置线程上下文类加载器,调用程序中的 main class

public static void main(String[] args) throws Exception {
	new JarLauncher().launch(args);
}
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
		throws Exception {
        //设置线程上下文类加载器
	Thread.currentThread().setContextClassLoader(classLoader);
	//调用MANIFEST.MF 中配置的Start-Class: xxx的main方法,还带入了参数
        createMainMethodRunner(mainClass, args, classLoader).run();
}
复制代码

相关推荐

Excel表格技巧—如何保留小数点后两位

在我们日常使用Ofiiceexcel中,经常会用到小数点保留,对此人们习惯性会使用单元格格式设置,不过这种办法并不是真正的四舍五入,而只是改变了excel的显示。我们下面要介绍的这个方式是利用函数做...

小数的意义和性质(十四)

27、在一个两位小数的末尾添上一个0,与原来小数相比,多了1107个计数单位,这个小数原来是多少?举个例子:0.35的末尾添上一个0,就变成了0.350,0.35里有35个0.01,0.350里有35...

必看!文职人员等级标志及有关问题20条!

来源:中国民兵微信公众号大道至简!为了让大家更多了解文职政策、报考文职的粉丝们最快、最直接掌握政策。首先是上独家解读和壁纸。然后,结合独家资料,梳理出20条有关文职的干货,快快收藏吧!1.文职人员的身...

小学数学单元知识体系概述-五年级《小数除法》

一、网络图二、知识点梳理及举例说明1.除数是整数。1)除到被除数的末尾没有余数,能除尽。举例:22.4÷4。...

“分数,小数,百分数 的互化”我这样教学,学生很快就学会了。

这部分知识千万不能忽视,用途可大了。我们在进行分数,小数,百分数四则混合运算时,不会三者之间的转化,很难把题计算准确。那么三者之间怎样转化呢?其实就这么几步:一,分数化小数,用分数的分子除以分数的分...

求小数近似数的最大值和最小值的技巧

例:一个两位小数,四舍五入后的近似数是5.0,这个三位小数最大可能是(),最小可能是()。分析:保留一位小数要看百分位上的数和5比较,若小于5的数舍去;若≥5的数,向前一位进一。四舍是最大,五入是...

实验室检测双乙酰的操作步骤

双乙酰学名是2,3丁二酮,是啤酒发酵过程中酵母自身代谢产生的一种副产物,当双乙酰的含量在浅色啤酒中超过0.15mg/L时,就会使啤酒产生一种令人不愉快的馊饭味,严重影响啤酒的质量和口感,在啤酒中双乙酰...

快来看!玉溪2020年中考分数段出炉

8月7日市教育体育局公布玉溪市2020年初中学业水平考试分数段据介绍,今年,玉溪市普通高中学校招生文化成绩满分为600分(不含照顾加分),由初中学业水平考试的13个学科成绩折算而得,总分累加保留两位小...

截取近似值的方法有几种?在生活中怎样应用这些方法?(21)

根据生活中的实际情况,截取近似值的方法有以下三种:(1)、四舍五入法:...

Excel数值取整及进位

取整数函数907.5;1034.2;1500要改变为908;1035;1500公式为:=CEILING(A1,1)907;1034;1500要改变为910;1040;1500公式为:...

C语言sprintf函数详解:“字符串魔法师”

♂?一句话理解sprintf「将数据按‘魔法咒语’(格式字符串)格式化,并将结果‘封印’到一个字符数组中!」...

sql server 数字四舍五入 保留两位小数

1.使用Round()函数,如Round(@num,2),其中参数2表示保留两位有效数字。缺点:Round()只是负责四舍五入到两位小数,但是不负责去掉后面的0。...

excel表格保留两位小数怎么保留?教您4种设置方法

保留两位小数怎么保留?有4种解决方法,我们来具体演示一下。·方法一:选择需要设置的单元格,然后点击开始菜单,数字功能区,点击增加小数位数或者是减少小数位数,调到保留两位就可以了。·方法二:选择需要设置...

如何对数字进行四舍五入,进一法,去尾法,精度法等不同方式取舍

指定位数取有效位针对数值我们有不同的处理方式,比如保留一位小数位,保留两位小数位,保留整数位等,一般我们都是采用四舍五入...

保留n位小数的函数公式

在日常办公中,Excel和WPS是我们处理数据的得力助手。今天,我们将介绍几个常用的函数,帮助你在数据处理中事半功倍。...