SpringBoot生成的jar包
Spring Boot的可执行jar包又称作“fat jar”,是包含所有三方依赖的jar。它与传统jar包最大的不同是包含了一个lib目录和内嵌了web容器。
可执行jar包的目录结构
通过maven命令打包后,会有2个jar包,一个为application-name.version-SNAPSHOT.jar,一个为application-name.version-SNAPSHOT.jar.original。后者仅包含应用编译后的本地资源,而前者引入了相关的第三方依赖。
将前者解压后的目录结构如下:
该目录比使用传统jar命令打包结构更复杂一些,目录含义如下:
- BOOT-INF/classes:目录存放应用编译后的class文件。
- BOOT-INF/lib:目录存放应用依赖的第三方JAR包文件。
- META-INF:目录存放应用打包信息(Maven坐标、pom文件)和MANIFEST.MF文件。
- org:目录存放SpringBoot相关class文件。
配置文件:MANIFEST.MF
MANIFEST.MF文件位于jar包的META-INF文件夹内,内容如下:
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By: cindy
Start-Class: com.shinemo.wangge.web.MainApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.3.3.RELEASE
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_212
Main-Class: org.springframework.boot.loader.JarLauncher
Main-Class这个属性对应的class的main方法是作为程序入口启动应用的。
Start-Class这个属性定义了我们项目的启动类。
可执行jar包启动器:JarLauncher
当使用java -jar命令执行Spring Boot应用的可执行jar文件时,该命令引导标准可执行的jar文件,读取在jar中META-INF/MANIFEST.MF文件的Main-Class属性值,该值代表应用程序执行入口类也就是包含main方法的类。
从MANIFEST.MF文件内容可以看到,Main-Class这个属性定义了org.springframework.boot.loader.JarLauncher,JarLauncher就是对应Jar文件的启动器。而我们项目的启动类MainApplication定义在Start-Class属性中,
JarLauncher会将BOOT-INF/classes下的类文件和BOOT-INF/lib下依赖的jar加入到classpath下,然后调用META-INF/MANIFEST.MF文件Start-Class属性完成应用程序的启动。
Launcher的继承关系如下:
启动器实现原理
启动类:JarLauncher
//JarLauncher.java
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
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);
}
}
JarLauncher默认构造函数实现为空,它父类ExecutableArchiveLauncher会调用再上一级父类Launcher的createArchive方法加载jar包, 加载了jar包之后,我们就能获取到里面所有的资源。
//JarLauncher.java
//JarLauncher默认构造函数
public JarLauncher() {
}
//ExecutableArchiveLauncher.java
public ExecutableArchiveLauncher() {
try {
//开始加载jar包
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
//Launcher.java
protected final Archive createArchive() throws Exception {
//通过获取当前Class类的信息,查找到当前归档文件的路径
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);
}
//如果是目录,则创建ExplodedArchive,否则创建JarFileArchive
return (root.isDirectory() ? new ExplodedArchive(root)
: new JarFileArchive(root));
}
核心方法:launch(String[] args)
launch方法实际上是调用父类Launcher的launch方法
// Launcher.java
protected void launch(String[] args) throws Exception {
//注册 Spring Boot 自定义的 URLStreamHandler 实现类,用于 jar 包的加载读取, 可读取到内嵌的jar包
JarFile.registerUrlProtocolHandler();
//创建自定义的 ClassLoader 实现类,用于从 jar 包中加载类。
ClassLoader classLoader = createClassLoader(getClassPathArchives());
//执行我们声明的 Spring Boot 启动类,进行 Spring Boot 应用的启动。
launch(args, getMainClass(), classLoader);
}
简单来说,就是创建一个可以读取 jar
包中类的加载器,保证 BOOT-INF/lib
目录下的类和 BOOT-classes
内嵌的 jar
中的类能够被正常加载到,之后执行 Spring Boot 应用的启动。
方法一:registerUrlProtocolHandler
JarFile.registerUrlProtocolHandler();
// JarFile.java
public static void registerUrlProtocolHandler() {
// 获得 URLStreamHandler 的路径
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
// 将 Spring Boot 自定义的 HANDLERS_PACKAGE(org.springframework.boot.loader) 补充上去
System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
: handlers + "|" + HANDLERS_PACKAGE));
// 重置已缓存的 URLStreamHandler 处理器们
resetCachedUrlHandlers();
}
该方法的目的就是通过将 org.springframework.boot.loader
包设置到 "java.protocol.handler.pkgs"
环境变量,从而使用到自定义的 URLStreamHandler 实现类 Handler,处理 jar:
协议的 URL。
利用java url协议实现扩展原理,自定义jar协议
将org.springframework.boot.loader包 追加到java系统 属性java.protocol.handler.pkgs中,实现自定义jar协议
java会在java.protocol.handler.pkgs系统属性指定的包中查找与协议同名的子包和名为Handler的类,
即负责处理当前协议的URLStreamHandler实现类必须在 <包名>.<协议名定义的包> 中,并且类名称必须为Handler
例如:
org.springframework.boot.loader.jar.Handler这个类 将用于处理jar协议
这个jar协议实现作用:
默认情况下,JDK提供的ClassLoader只能识别jar中的class文件以及加载classpath下的其他jar包中的class文件。
对于jar包中的jar包是无法加载的
所以spring boot 自己定义了一套URLStreamHandler实现类和JarURLConnection实现类,用来加载jar包中的jar包的class类文件
举个例子:
jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/lib/spring-boot-1.5.10.RELEASE.jar!/
jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/lib/spring-boot-1.5.10.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class
我们看到如果有 jar 包中包含 jar,或者 jar 包中包含 jar 包里面的 class 文件,那么会使用 !/分隔开,这种方式只有 org.springframework.boot.loader.jar.Handler 能处理,它是 SpringBoot 内部扩展出来一种URL协议.
通常,jar里的资源分隔符是!/,在JDK提供的JarFile URL只支持一层“!/”,而Spring Boot扩展了该协议,可支持多层“!/”。
方法二:createClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchives());
getClassPathArchives()
// ExecutableArchiveLauncher.java
@Override
protected List<Archive> getClassPathArchives() throws Exception {
// <1> 获得所有 Archive
List<Archive> archives = new ArrayList<>(
this.archive.getNestedArchives(this::isNestedArchive));
// <2> 后续处理:是个空方法
postProcessClassPathArchives(archives);
return archives;
}
<1>
处,this::isNestedArchive
代码段,创建了 EntryFilter 匿名实现类,用于过滤 jar
包不需要的目录。目的就是过滤获得,BOOT-INF/classes/
目录下的类,以及 BOOT-INF/lib/
的内嵌 jar
包。
// JarLauncher.java
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
// 如果是目录的情况,只要 BOOT-INF/classes/ 目录
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
// 如果是文件的情况,只要 BOOT-INF/lib/ 目录下的 `jar` 包
return entry.getName().startsWith(BOOT_INF_LIB);
}
<1>处getNestedArchives()方法实现
//JarFileArchive.java
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
createClassLoader(List archives)
// ExecutableArchiveLauncher.java
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
// 获得所有 Archive 的 URL 地址
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
// 创建加载这些 URL 的 ClassLoader
return createClassLoader(urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
基于获得的 Archive 数组,创建自定义 ClassLoader 实现类 LaunchedURLClassLoader,通过它来加载 BOOT-INF/classes
目录下的类,以及 BOOT-INF/lib
目录下的 jar
包中的类。
方法三:launch(String[] args, String mainClass, ClassLoader classLoader)
launch(args, getMainClass(), classLoader);
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
// <1> 设置 LaunchedURLClassLoader 作为类加载器
Thread.currentThread().setContextClassLoader(classLoader);
// <2> 创建 MainMethodRunner 对象,并执行 run 方法,启动 Spring Boot 应用
createMainMethodRunner(mainClass, args, classLoader).run();
}
<1>
处:设置 LaunchedURLClassLoader 作为类加载器,从而保证能够从 jar
包中加载到相应的类。
getMainClass()
// ExecutableArchiveLauncher.java
@Override
protected String getMainClass() throws Exception {
// 获得启动的类的全名
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException(
"No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
从 jar
包的 MANIFEST.MF
文件的 Start-Class
配置项,,获得我们设置的 Spring Boot 的主启动类。
createMainMethodRunner
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
run()
public void run() throws Exception {
// <1> 加载 Spring Boot
Class<?> mainClass = Thread.currentThread().getContextClassLoader()
.loadClass(this.mainClassName);
// <2> 反射调用 main 方法
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
该方法负责最终的 Spring Boot 应用真正的启动。
SpringBoot自定义的类加载器: LaunchedURLClassLoader
LaunchedURLClassLoader 是 spring-boot-loader
项目自定义的类加载器,实现对 jar
包中 META-INF/classes
目录下的类和 META-INF/lib
内嵌的 jar
包中的类的加载。
该ClassLoader继承自UrlClassLoader。UrlClassLoader加载class就是依靠初始参数传入的Url数组,并且尝试从Url指向的资源中加载Class文件
//LaunchedURLClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Handler.setUseFastConnectionExceptions(true);
try {
try {
//尝试根据类名去定义类所在的包,即java.lang.Package,确保jar in jar里匹配的manifest能够和关联的package关联起来
definePackageIfNecessary(name);
}
catch (IllegalArgumentException ex) {
// Tolerate race condition due to being parallel capable
if (getPackage(name) == null) {
// This should never happen as the IllegalArgumentException indicates
// that the package has already been defined and, therefore,
// getPackage(name) should not return null.
//这里异常表明,definePackageIfNecessary方法的作用实际上是预先过滤掉查找不到的包
throw new AssertionError("Package " + name + " has already been "
+ "defined but it could not be found");
}
}
return super.loadClass(name, resolve);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
方法super.loadClass(name, resolve)实际上会回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循双亲委派机制进行查找类,而Bootstrap ClassLoader和Extension ClassLoader将会查找不到fat jar依赖的类,最终会来到Application ClassLoader,调用java.net.URLClassLoader#findClass
为什么要引入自定义类加载器
因为SpringBoot
实现了Jar包的嵌套,一个Jar包就可以完成整个程序的运行。
引入自定义类加载器就是为了能解决jar包嵌套jar包的问题,系统自带的AppClassLoarder不支持读取嵌套jar包
为什么SpringBoot要将Loader 类下的所有文件复制出来呢
因为程序毕竟要有一个启动入口,这个入口要由应用类加载器加载,先将SpringBoot Class Loader加载到内存中,然后通过后续的一些操作创建线程上下文加载器,去加载第三方jar。
如果将SpringBoot Class Loader
也放到lib文件下,是根本无法被加载到的,因为它根本不符合jar文件的一个标准规范