彻底搞懂 SpringBoot jar 可执行原理
大家好,我是宝哥!
文章篇幅较长,但是包含了SpringBoot 可执行jar包从头到尾的原理,请读者耐心观看。
spring-boot-maven-plugin
all-in-one jar
包。maven-jar-plugin
生成的包和spring-boot-maven-plugin
生成的包之间的直接区别,是fat jar
中主要增加了两部分,第一部分是lib目录,存放的是Maven依赖的jar包文件,第二部分是spring boot loader相关的类。fat jar //目录结构
├─BOOT-INF
│ ├─classes
│ └─lib
├─META-INF
│ ├─maven
│ ├─app.properties
│ ├─MANIFEST.MF
└─org
└─springframework
└─boot
└─loader
├─archive
├─data
├─jar
└─util
spring-boot-maven-plugin
工作机制,而spring-boot-maven-plugin
属于自定义插件,因此我们又必须知道,Maven的自定义插件是如何工作的Maven的自定义插件
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
org.springframework.boot.maven.RepackageMojo#execute
,该方法的主要逻辑是调用了org.springframework.boot.maven.RepackageMojo#repackage
private void repackage() throws MojoExecutionException {
//获取使用maven-jar-plugin生成的jar,最终的命名将加上.orignal后缀
Artifact source = getSourceArtifact();
//最终文件,即Fat jar
File target = getTargetFile();
//获取重新打包器,将重新打包成可执行jar文件
Repackager repackager = getRepackager(source.getFile());
//查找并过滤项目运行时依赖的jar
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
getFilters(getAdditionalFilters()));
//将artifacts转换成libraries
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
getLog());
try {
//提供Spring Boot启动脚本
LaunchScript launchScript = getLaunchScript();
//执行重新打包逻辑,生成最后fat jar
repackager.repackage(target, libraries, launchScript);
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
//将source更新成 xxx.jar.orignal文件
updateArtifact(source, target, repackager.getBackupFile());
}
org.springframework.boot.maven.RepackageMojo#getRepackager
这个方法,知道Repackager是如何生成的,也就大致能够推测出内在的打包逻辑。private Repackager getRepackager(File source) {
Repackager repackager = new Repackager(source, this.layoutFactory);
repackager.addMainClassTimeoutWarningListener(
new LoggingMainClassTimeoutWarningListener());
//设置main class的名称,如果不指定的话则会查找第一个包含main方法的类,repacke最后将会设置org.springframework.boot.loader.JarLauncher
repackager.setMainClass(this.mainClass);
if (this.layout != null) {
getLog().info("Layout: " + this.layout);
//重点关心下layout 最终返回了 org.springframework.boot.loader.tools.Layouts.Jar
repackager.setLayout(this.layout.layout());
}
return repackager;
}
/**
* Executable JAR layout.
*/
public static class Jar implements RepackagingLayout {
@Override
public String getLauncherClassName() {
return "org.springframework.boot.loader.JarLauncher";
}
@Override
public String getLibraryDestination(String libraryName, LibraryScope scope) {
return "BOOT-INF/lib/";
}
@Override
public String getClassesLocation() {
return "";
}
@Override
public String getRepackagedClassesLocation() {
return "BOOT-INF/classes/";
}
@Override
public boolean isExecutable() {
return true;
}
}
org.springframework.boot.loader.JarLauncher
,从名字推断,这很可能是返回可执行jar文件的启动类。MANIFEST.MF文件内容
Manifest-Version: 1.0
Implementation-Title: oneday-auth-server
Implementation-Version: 1.0.0-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: oneday
Implementation-Vendor-Id: com.oneday
Spring-Boot-Version: 2.1.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.oneday.auth.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_171
MANIFEST.MF
文件为以上信息,可以看到两个关键信息Main-Class
和Start-Class
。我们可以进一步,程序的启动入口并不是我们SpringBoot中定义的main,而是JarLauncher#main
,而再在其中利用反射调用定义好的Start-Class
的main方法JarLauncher
java.util.jar.JarFile JDK
工具类提供的读取jar文件org.springframework.boot.loader.jar.JarFileSpringboot-loader
继承JDK提供JarFile类java.util.jar.JarEntryDK
工具类提供的jar文件条目org.springframework.boot.loader.jar.JarEntry Springboot-loader
继承JDK提供JarEntry类org.springframework.boot.loader.archive.Archive
Springboot抽象出来的统一访问资源的层JarFileArchivejar
包文件的抽象ExplodedArchive
文件目录
JarFile
的作用,每个JarFileArchive
都会对应一个JarFile。在构造的时候会解析内部结构,去获取jar包里的各个文件或文件夹类。我们可以看一下该类的注释。/* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality.
* <ul>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
* on any directory entry.</li>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
* embedded JAR files (as long as their entry is not compressed).</li>
</ul>
**/
自定义类加载机制
最基础:
Bootstrap ClassLoader
(加载JDK的/lib目录下的类)次基础:
Extension ClassLoader
(加载JDK的/lib/ext目录下的类)普通:
Application ClassLoader
(程序自己classpath下的类)
ClassLoader
加载,就不能让高层的ClassLoader
加载,这样是为了范围错误的引入了非JDK下但是类名一样的类。LaunchedURLClassLoader
,该类继承了java.net.URLClassLoader
,重写了java.lang.ClassLoader#loadClass(java.lang.String, boolean)
,然后我们再探讨他是如何修改双亲委派机制。LaunchedURLClassLoader
的构造方法。public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
the URLs from which to load classes and resources
,即fat jar包依赖的所有类和资源,将该urls参数传递给父类java.net.URLClassLoader
,由父类的java.net.URLClassLoader#findClass
执行查找类方法,该类的查找来源即构造方法传递进来的urls参数。//LaunchedURLClassLoader的实现
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
如何真正的启动
org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)
反射调用逻辑比较简单,这里就不再分析,比较关键的一点是,在调用main方法之前,将当前线程的上下文类加载器设置成LaunchedURLClassLoader
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
Demo
public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
JarFile.registerUrlProtocolHandler();
// 构造LaunchedURLClassLoader类加载器,这里使用了2个URL,分别对应jar包中依赖包spring-boot-loader和spring-boot,使用 "!/" 分开,需要org.springframework.boot.loader.jar.Handler处理器处理
LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(
new URL[] {
new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-loader-1.2.3.RELEASE.jar!/")
, new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-2.1.3.RELEASE.jar!/")
},
Application.class.getClassLoader());
// 加载类
// 这2个类都会在第二步本地查找中被找出(URLClassLoader的findClass方法)
classLoader.loadClass("org.springframework.boot.loader.JarLauncher");
classLoader.loadClass("org.springframework.boot.SpringApplication");
// 在第三步使用默认的加载顺序在ApplicationClassLoader中被找出
classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");
// SpringApplication.run(Application.class, args);
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
总结
对于源码分析,这次的较大收获则是不能一下子去追求弄懂源码中的每一步代码的逻辑,即便我知道该方法的作用。我们需要搞懂的是关键代码,以及涉及到的知识点。
我从Maven的自定义插件开始进行追踪,巩固了对Maven的知识点,在这个过程中甚至了解到JDK对jar的读取是有提供对应的工具类。最后最重要的知识点则是自定义类加载器。整个代码下来并不是说代码究竟有多优秀,而是要学习他因何而优秀。
来源:juejin.im/post/5d2d6812e51d45777b1a3e5a
精彩推荐:
从零开始搭建公司大型SaaS 平台架构技术栈,这套架构绝了!
评论