Spring Boot FatJar 类加载机制简要分析
Java 类加载机制
Java 中通过类加载器实现类的加载,类加载器位于 JVM 之外,通过“一个类的全限定名来获取描述此类的二进制字节流”。如下为ClassLoader
抽象类中loadClass
方法签名,可见,loadClass
方法返回一个Class<?>
对象,如果按照名称加载类失败,则抛出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
类初始化过程中,会依次初始化ExtClassLoader
和AppClassLoader
。
双亲委派模型
如前所述,Java 虚拟机中类是由其本身和加载其的类加载器唯一确定。类加载器的双亲委派模型很好地实现了这一限制。
双亲委派模型是指,任何一个类加载器在加载某个类时,会首先将加载请求委托给父类加载器实现,如果父类加载器未成功加载,再尝试由自己加载。
各个类加载器的父类加载器并不是通过类继承的方式实现,而是通过ClassLoader
的parent
属性指定,通过构造函数传入:
各个类加载器之间的父子关系如图,可见,所有类的加载最终都会被代理至启动类加载器,用户自定义类加载器可以通过继承应用类加载器实现。
类加载调用流程
双亲委派描述的:将类加载请求首先委托给父类加载器,如果父类加载器加载失败,再执行自身的加载流程。可以在ClassLoader
的如下代码中看到:
上述代码中可以看到,类加载器本身的加载逻辑是由方法findClass
完成的,在ClassLoader
类中该方法的定义如下,具体逻辑交给具体实现类实现:
可见,在用户编写自定义类加载器时,只需要实现findClass
方法就符合双亲委派的要求。
Thread Context ClassLoader 与 SPI 机制
在Thread
类中,可以看到有属性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
方法:
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 应用启动所需的基础类
在META-INF
下的MANIFEST.MF
jar 描述信息文件示例如下,其中主要包括:
Main-Class
指定的启动类入口,对于 Spring Boot 应用来说,是固定的Start-Class
指定的应用启动类入口,由用户定义,通常位于应用最上一层的包下其他应用名称、版本信息,以及构建信息等
对于一个标准的可执行 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 文件中规定的依赖顺序。
jar 文件是一种特殊的 zip 文件,而 zip 文件中包含了其中各个文件的顺序信息,此顺序信息并非按照字母序或者文件的创建时间等顺序,而是写入压缩文件的顺序,相关信息可以参考 zip 文件的格式。
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 加载应用定义的主函数,通过反射的方式触发主函数调用。
版权声明: 本文为 InfoQ 作者【luojiahu】的原创文章。
原文链接:【http://xie.infoq.cn/article/e4919a2941d5178de7af3548d】。文章转载请联系作者。
评论