写点什么

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

用户头像
luojiahu
关注
发布于: 2021 年 06 月 02 日
Spring Boot FatJar类加载机制简要分析

Java 类加载机制

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


public Class<?> loadClass(String name) throws ClassNotFoundException;
复制代码


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

类加载器的种类

  • 启动类加载器(Bootstrap ClassLoader)

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

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

  • 扩展类加载器(Extension ClassLoader)

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


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

  • 应用程序类加载器(Application ClassLoader)

应用程序类加载器定义位于sun.misc.Launcher.AppClassLoader,其类继承关系如下,可以看到与ExtClassLoader类似:


应用程序类加载器负责加载由CLASS_PATH指定的类库,如果程序中没有自定义的类加载器,则一般情况下这就是默认的类加载器。

应用程序类加载器可以通过如下方法获取:java.lang.ClassLoader#getSystemClassLoader;跟踪代码可以看到,在Launcher类初始化过程中,会依次初始化ExtClassLoaderAppClassLoader

双亲委派模型

如前所述,Java 虚拟机中类是由其本身和加载其的类加载器唯一确定。类加载器的双亲委派模型很好地实现了这一限制。


双亲委派模型是指,任何一个类加载器在加载某个类时,会首先将加载请求委托给父类加载器实现,如果父类加载器未成功加载,再尝试由自己加载。


各个类加载器的父类加载器并不是通过类继承的方式实现,而是通过ClassLoaderparent属性指定,通过构造函数传入:


// 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);}
复制代码


各个类加载器之间的父子关系如图,可见,所有类的加载最终都会被代理至启动类加载器,用户自定义类加载器可以通过继承应用类加载器实现。


类加载调用流程

双亲委派描述的:将类加载请求首先委托给父类加载器,如果父类加载器加载失败,再执行自身的加载流程。可以在ClassLoader的如下代码中看到:


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; }}
复制代码


上述代码中可以看到,类加载器本身的加载逻辑是由方法findClass完成的,在ClassLoader类中该方法的定义如下,具体逻辑交给具体实现类实现:


protected Class<?> findClass(String name) throws ClassNotFoundException {    throw new ClassNotFoundException(name);}
复制代码


可见,在用户编写自定义类加载器时,只需要实现findClass方法就符合双亲委派的要求。

Thread Context ClassLoader 与 SPI 机制

Thread类中,可以看到有属性contextClassLoader和对应的获取方法:


/* 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 可以通过将对应的类以全限定名的形式指定,相关指定文件置于META-INF/services/下,这样在程序启动时,便可以加载自定义的 SPI。自定义的 SPI 类加载逻辑是在ServiceLoader类中实现的,该类位于rt.jar中,因此是由启动类加载器加载的,但是,用户自定义类并不能被启动类加载器所加载。


为了解决这个问题,引入了 Thread Context ClassLoader,通过在ServiceLoader中设置指定的类加载器,便可以实现自定义 SPI 类的加载,下面是ServiceLoader中的load方法:


public static <S> ServiceLoader<S> load(Class<S> service) {    ClassLoader cl = Thread.currentThread().getContextClassLoader();    return ServiceLoader.load(service, cl);}
复制代码


Thread Context ClassLoader 在初始化阶段会被制定为父线程的 Context ClassLoader,默认情况下,是应用类加载器AppClassLoader,因而,可以实现加载用户指定的 SPI 类。

Spring Boot FatJar 类加载机制分析

Spring Boot 自定义了 FatJar 的应用部署态打包形式,通过将所有依赖、配置打包到一个统一的 jar 包,降低了在应用部署时的复杂度,方便应用接入持续集成、Devops 等流程。


一个典型的 FatJar 结构如下。可以看到,FatJar 主要由及部分组成:


  • BOOT-INF下的classes目录,主要包括应用本身的配置和编译后的 class 文件

  • BOOT-INF下的lib目录,包括应用依赖的 jar 包

  • META-INF下的MANIFEST.MF jar 描述信息文件

  • META-INF下的maven目录,包括 maven 管理的依赖及打包基础信息

  • META-INF下的spring-configuration-metadata.json,描述了应用自定义的配置类信息

  • META-INF下的org.springframework.boot,主要包括 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            |....
复制代码


META-INF下的MANIFEST.MF jar 描述信息文件示例如下,其中主要包括:


  • Main-Class指定的启动类入口,对于 Spring Boot 应用来说,是固定的

  • Start-Class指定的应用启动类入口,由用户定义,通常位于应用最上一层的包下

  • 其他应用名称、版本信息,以及构建信息等


manifest-Version: 1.0Implementation-Title: my-tsf-app-aImplementation-Version: 1.0-SNAPSHOTStart-Class: com.bocsoft.xiaohu.tsf.appa.AppaApplication  # 应用启动入口类Spring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/Build-Jdk-Spec: 1.8Spring-Boot-Version: 2.1.6.RELEASECreated-By: Maven Archiver 3.4.0Main-Class: org.springframework.boot.loader.JarLauncher  # spring boot 启动入口
复制代码


对于一个标准的可执行 jar 包来说,位于META-INF下的MANIFEST.MF制定了主函数类的位置,从而 java 可以通过主函数开始启动应用。


一个应用默认的类加载器是AppClassLoader,负责加载CLASS_PATH下的所有类。对于 Spring Boot 的 FatJar 来说,问题在于:应用所依赖的 jar 包都位于 fatjar 内部,这些 jar 不会被AppClassLoader加载,因此需要考虑在应用启动的时候,将这些依赖的 jar 包中的类都进行加载。


Spring Boot 通过 Thread Context ClassLoader 实现自定义的类加载器,从而解决上述问题。


JarLauncher类的main函数作为入口开始,可以看到,在启动过程中主要包含如下关键流程:


  • org.springframework.boot.loader.Launcher#launch(java.lang.String[])方法中,首先会创建 ClassLoader,作为后续启动参数

  • createClassLoader()方法实际返回的类为LaunchedURLClassLoader

  • 跟踪getClassPathArchives()函数可以看到,创建 ClassLoader 时,会将 FatJar 中的所有 class 文件和 jar 传入,作为 ClassLoader 创建的入参,从而LaunchedURLClassLoader可以实现应用所依赖的其他 jar 中包含类的加载。

  • 跟踪getClassPathArchives()函数可以看到,在加载应用所依赖的 jar 信息时,是按照 jar 包中文件的顺序进行加载的,而 jar 包中的顺序在通过spring-boot-maven-plugin打包时进行了定义,顺序是应用中按照 pom 文件中规定的依赖顺序。


  1. jar 文件是一种特殊的 zip 文件,而 zip 文件中包含了其中各个文件的顺序信息,此顺序信息并非按照字母序或者文件的创建时间等顺序,而是写入压缩文件的顺序,相关信息可以参考 zip 文件的格式。

  2. Spring Boot 启动时解析 jar 中文件顺序的代码主要位于org.springframework.boot.loader.jar.CentralDirectoryParser类中


  • org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)方法中,会将LaunchedURLClassLoader作为 Thread Context ClassLoader 进行设置

  • org.springframework.boot.loader.MainMethodRunner#run方法中,通过 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 }); }
复制代码


发布于: 2021 年 06 月 02 日阅读数: 249
用户头像

luojiahu

关注

喜欢思考组织、过程、产品的后端开发 2017.01.08 加入

还未添加个人简介

评论

发布
暂无评论
Spring Boot FatJar类加载机制简要分析