Spring Boot FatJar类加载机制简要分析
Java类加载机制
Java中通过类加载器实现类的加载,类加载器位于JVM之外,通过“一个类的全限定名来获取描述此类的二进制字节流”。如下为抽象类中方法签名,可见,方法返回一个对象,如果按照名称加载类失败,则抛出。
public Class> loadClass(String name) throws ClassNotFoundException;
在Java虚拟机中,一个类由类本身和加载他的类加载器唯一确定,每一个类加载器,都拥有一个独立的类名称空间。也就是说,如果同一个类被不同的类加载器加载,那么这两个不相等。
类加载器的种类
启动类加载器(Bootstrap ClassLoader)
启动类加载器是Java虚拟机实现的一部分,采用C++实现,用户在Java程序中无法直接引用启动类加载器。
其加载的类范围是位于
扩展类加载器(Extension ClassLoader)
扩展类加载器的定义位于,其类继承关系如下:

扩展类加载器负责加载位于
应用程序类加载器(Application ClassLoader)
应用程序类加载器定义位于,其类继承关系如下,可以看到与类似:

应用程序类加载器负责加载由指定的类库,如果程序中没有自定义的类加载器,则一般情况下这就是默认的类加载器。
应用程序类加载器可以通过如下方法获取:;跟踪代码可以看到,在类初始化过程中,会依次初始化和。
双亲委派模型
如前所述,Java虚拟机中类是由其本身和加载其的类加载器唯一确定。类加载器的双亲委派模型很好地实现了这一限制。
双亲委派模型是指,任何一个类加载器在加载某个类时,会首先将加载请求委托给父类加载器实现,如果父类加载器未成功加载,再尝试由自己加载。
各个类加载器的父类加载器并不是通过类继承的方式实现,而是通过的属性指定,通过构造函数传入:
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
各个类加载器之间的父子关系如图,可见,所有类的加载最终都会被代理至启动类加载器,用户自定义类加载器可以通过继承应用类加载器实现。

类加载调用流程
双亲委派描述的:将类加载请求首先委托给父类加载器,如果父类加载器加载失败,再执行自身的加载流程。可以在的如下代码中看到:
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
// 首先委托给父类加载器进行加载
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果父类加载器加载结果为空,再执行自身加载逻辑
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
上述代码中可以看到,类加载器本身的加载逻辑是由方法完成的,在类中该方法的定义如下,具体逻辑交给具体实现类实现:
protected Class> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可见,在用户编写自定义类加载器时,只需要实现方法就符合双亲委派的要求。
Thread Context ClassLoader与SPI机制
在类中,可以看到有属性和对应的获取方法:
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
Thread Context ClassLoader主要是为了解决Java SPI机制中加载用户自定义SPI(Service Provider Interface)类的问题。
自定义SPI可以通过将对应的类以全限定名的形式指定,相关指定文件置于下,这样在程序启动时,便可以加载自定义的SPI。自定义的SPI类加载逻辑是在类中实现的,该类位于中,因此是由启动类加载器加载的,但是,用户自定义类并不能被启动类加载器所加载。
为了解决这个问题,引入了Thread Context ClassLoader,通过在中设置指定的类加载器,便可以实现自定义SPI类的加载,下面是中的方法:
public static ServiceLoader load(Class service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Thread Context ClassLoader在初始化阶段会被制定为父线程的Context ClassLoader,默认情况下,是应用类加载器,因而,可以实现加载用户指定的SPI类。
Spring Boot FatJar类加载机制分析
Spring Boot自定义了FatJar的应用部署态打包形式,通过将所有依赖、配置打包到一个统一的jar包,降低了在应用部署时的复杂度,方便应用接入持续集成、Devops等流程。
一个典型的FatJar结构如下。可以看到,FatJar主要由及部分组成:
下的目录,主要包括应用本身的配置和编译后的class文件
下的目录,包括应用依赖的jar包
下的 jar描述信息文件
下的目录,包括maven管理的依赖及打包基础信息
下的,描述了应用自定义的配置类信息
下的,主要包括Spring Boot应用启动所需的基础类
├── BOOT-INF
│ ├── classes # 应用配置和class文件
│ │ ├── application.yml
│ │ ├── com
│ │ │ └── mycompany
│ │ │ └── xiaohu
│ │ │ └── frame
│ │ │ └── appa
│ │ │ ├── AppaApplication.class
| | | ├── ...
│ │ └── log
│ │ └── log4j2.yml
│ └── lib # 应用依赖jar文件
│ ├── some-dependency-a.jar
│ ├── some-dependency-b.jar
| |....
├── META-INF
│ ├── MANIFEST.MF # jar 描述信息文件
│ ├── maven # maven 信息文件
│ │ └── com.mycompany.xiaohu
│ │ └── my-frame-app-a
│ │ ├── pom.properties
│ │ └── pom.xml
│ └── spring-configuration-metadata.json # 应用自定义配置信息文件
└── org
└── springframework
└── boot
└── loader # spring boot 启动相关类库
| ├── archive
| │ ├── Archive.class
|....
在下的 jar描述信息文件示例如下,其中主要包括:
指定的启动类入口,对于Spring Boot 应用来说,是固定的
指定的应用启动类入口,由用户定义,通常位于应用最上一层的包下
其他应用名称、版本信息,以及构建信息等
manifest-Version: 1.0
Implementation-Title: my-tsf-app-a
Implementation-Version: 1.0-SNAPSHOT
Start-Class: com.bocsoft.xiaohu.tsf.appa.AppaApplication # 应用启动入口类
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.6.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher # spring boot 启动入口
对于一个标准的可执行jar包来说,位于下的制定了主函数类的位置,从而java可以通过主函数开始启动应用。
一个应用默认的类加载器是,负责加载下的所有类。对于Spring Boot的FatJar来说,问题在于:应用所依赖的jar包都位于fatjar内部,这些jar不会被加载,因此需要考虑在应用启动的时候,将这些依赖的jar包中的类都进行加载。
Spring Boot通过Thread Context ClassLoader实现自定义的类加载器,从而解决上述问题。
从类的函数作为入口开始,可以看到,在启动过程中主要包含如下关键流程:
在方法中,首先会创建ClassLoader,作为后续启动参数
方法实际返回的类为
跟踪函数可以看到,创建ClassLoader时,会将FatJar中的所有class文件和jar传入,作为ClassLoader创建的入参,从而可以实现应用所依赖的其他jar中包含类的加载。
跟踪函数可以看到,在加载应用所依赖的jar信息时,是按照jar包中文件的顺序进行加载的,而jar包中的顺序在通过打包时进行了定义,顺序是应用中按照pom文件中规定的依赖顺序。
在方法中,会将作为Thread Context ClassLoader进行设置
在方法中,通过Thread Context ClassLoader加载应用定义的主函数,通过反射的方式触发主函数调用。
// org.springframework.boot.loader.JarLauncher类的方法
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
// org.springframework.boot.loader.Launcher类的方法
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
// org.springframework.boot.loader.Launcher类的方法
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
// org.springframework.boot.loader.MainMethodRunner#run 方法
public void run() throws Exception {
Class> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}