Bootstrap

Spring Boot FatJar类加载机制简要分析

Java类加载机制

Java中通过类加载器实现类的加载,类加载器位于JVM之外,通过“一个类的全限定名来获取描述此类的二进制字节流”。如下为抽象类中方法签名,可见,方法返回一个对象,如果按照名称加载类失败,则抛出。

public Class loadClass(String name) throws ClassNotFoundException;

在Java虚拟机中,一个类由类本身和加载他的类加载器唯一确定,每一个类加载器,都拥有一个独立的类名称空间。也就是说,如果同一个类被不同的类加载器加载,那么这两个不相等。

类加载器的种类

  • 启动类加载器(Bootstrap ClassLoader)

启动类加载器是Java虚拟机实现的一部分,采用C++实现,用户在Java程序中无法直接引用启动类加载器。

其加载的类范围是位于/lib下,或者通过启动参数-Xbootclasspath指定目录下的类库。这些类库还需要能够被虚拟机识别,例如rt.jar等。

  • 扩展类加载器(Extension ClassLoader)

扩展类加载器的定义位于,其类继承关系如下:

扩展类加载器负责加载位于/lib/ext目录下,或者由java.ext.dirs系统变量制定的目录下的类库。

  • 应用程序类加载器(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 });
  }