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

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

wxin55 2025-05-11 19:02 3 浏览 0 评论

一、概述

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 依赖,以便查看源码和远程调试。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-loader</artifactId>
</dependency>
复制代码

认真比较可以看出,这个 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<Archive> getClassPathArchives() throws Exception {
	List<Archive> 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<Archive> archives) throws Exception {
	List<URL> 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();
}
复制代码

相关推荐

Java框架 —— Spring简介

简介一般来说,Spring指的是SpringFramework,它提供了很多功能,例如:控制反转(IOC)、依赖注入(DI)、切面编程(AOP)、事务管理(TX)主要jar包org.sprin...

Monkey自动化测试

Monkey1.通过Monkey程序模拟用户触摸屏幕、滑动Trackball、按键等操作来对设备上的程序进行压力测试,检测程序多久的时间会发生异常;2.Monkey主要用于Android的压力...

十年之重修SpringBoot启动&amp;自动装载&amp;Bean加载过程

总结Springboot的自动装载,完全是依赖Bean的自动注册,其中默认的规则,是把需要自动装载的bean全名称编辑在spring.factories(2.7之后的版本,还支持.imports文件)...

一些可以显著提高大型 Java 项目启动速度的尝试

我们线上的业务jar包基本上普遍比较庞大,动不动一个jar包上百M,启动时间在分钟级,拖慢了我们在故障时快速扩容的响应。于是做了一些分析,看看Java程序启动慢到底慢在哪里,如何去优化,...

class 增量发包改造为 jar 包方式发布

大纲class增量发包介绍项目目录结构介绍jar包方式发布落地方案class增量发包介绍当前项目的迭代修复都是通过class增量包来发版本的将改动的代码class增量打包,如下图cla...

Flink架构及其工作原理(很详细)

原文链接:https://www.cnblogs.com/code2one/p/10123112.html关键词:Flink架构、面试杀手锏!更多大数据架构、实战经验,欢迎关注【大数据与机器学习】,...

大促系统优化之应用启动速度优化实践

作者:京东零售宋维飞一、前言本文记录了在大促前针对SpringBoot应用启动速度过慢而采取的优化方案,主要介绍了如何定位启动速度慢的阻塞点,以及如何解决这些问题。希望可以帮助大家了解如何定位该类问...

Maven工程如何使用非Maven仓库jar包

使用Maven之前,一直都是自己手工在网上搜索需要的jar包,然后添加到工程中。以这样的方式开发,工作了好多年,曾经以为以后也会一直这样下去。直到碰上Maven,用了第一次,就抛弃老方法了。Maven...

【推荐】一款开源免费、功能强大的短链接生成平台

项目介绍reduce是一款开源免费、功能强大的短链接生成平台。部署在服务器,使用短域名解析即可提供服务。CoodyFramework首秀,自写IOC、MVC、ORM、TASK、JSON、DB连接池、...

K8S官方java客户端之七:patch操作

欢迎访问我的GitHubhttps://github.com/zq2599/blog_demos内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;...

Java 的业务逻辑验证框架 之-fluent-validator

开发人员在维护核心业务逻辑的同时,还需要为输入做严格的校验。当输入不合法时,能够给caller一个明确的反馈,最常见的反馈就是返回封装了result的对象或者抛出exception。一些常见...

互联网大厂后端必看!手把手教你替换 Spring Boot 中的日志框架

在互联网大厂的后端开发工作中,SpringBoot框架是搭建项目的“得力助手”,使用十分普遍。但不少开发者都遇到过这样的困扰:SpringBoot默认集成的Logback日志框架,在实际...

测试经理教你如何用monkey进行压力测试!

一、monkey是什么1、monkey程序由android系统自带,使用Java语言写成,在Android文件系统中的存放路径是:/system/framework/monkey.jar2、Mo...

Java-Maven详解

一、什么是Maven?ApacheMaven是一个软件项目管理的综合工具。基于项目对象模型(POM)的概念,提供了帮助管理构建、文档、报告、依赖、发布等方法,Maven简化和标准化项目建设过程。处理...

SpringBoot打包部署最佳实践

springboot介绍SpringBoot目前流行的javaweb应用开发框架,相比传统的spring开发,springboot极大简化了配置,并且遵守约定优于配置的原则即使0配置也能正常运...

取消回复欢迎 发表评论: