徹底搞懂 SpringBoot jar 可執(zhí)行原理
大家好,我是寶哥!
文章篇幅較長(zhǎng),但是包含了SpringBoot 可執(zhí)行jar包從頭到尾的原理,請(qǐng)讀者耐心觀看。
spring-boot-maven-plugin
all-in-one jar 包。maven-jar-plugin生成的包和spring-boot-maven-plugin生成的包之間的直接區(qū)別,是fat jar中主要增加了兩部分,第一部分是lib目錄,存放的是Maven依賴的jar包文件,第二部分是spring boot loader相關(guān)的類。fat jar //目錄結(jié)構(gòu)
├─BOOT-INF
│ ├─classes
│ └─lib
├─META-INF
│ ├─maven
│ ├─app.properties
│ ├─MANIFEST.MF
└─org
└─springframework
└─boot
└─loader
├─archive
├─data
├─jar
└─util
spring-boot-maven-plugin工作機(jī)制,而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,該方法的主要邏輯是調(diào)用了org.springframework.boot.maven.RepackageMojo#repackageprivate void repackage() throws MojoExecutionException {
//獲取使用maven-jar-plugin生成的jar,最終的命名將加上.orignal后綴
Artifact source = getSourceArtifact();
//最終文件,即Fat jar
File target = getTargetFile();
//獲取重新打包器,將重新打包成可執(zhí)行jar文件
Repackager repackager = getRepackager(source.getFile());
//查找并過(guò)濾項(xiàng)目運(yùn)行時(shí)依賴的jar
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
getFilters(getAdditionalFilters()));
//將artifacts轉(zhuǎn)換成libraries
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
getLog());
try {
//提供Spring Boot啟動(dòng)腳本
LaunchScript launchScript = getLaunchScript();
//執(zhí)行重新打包邏輯,生成最后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這個(gè)方法,知道Repackager是如何生成的,也就大致能夠推測(cè)出內(nèi)在的打包邏輯。private Repackager getRepackager(File source) {
Repackager repackager = new Repackager(source, this.layoutFactory);
repackager.addMainClassTimeoutWarningListener(
new LoggingMainClassTimeoutWarningListener());
//設(shè)置main class的名稱,如果不指定的話則會(huì)查找第一個(gè)包含main方法的類,repacke最后將會(huì)設(shè)置org.springframework.boot.loader.JarLauncher
repackager.setMainClass(this.mainClass);
if (this.layout != null) {
getLog().info("Layout: " + this.layout);
//重點(diǎn)關(guān)心下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,從名字推斷,這很可能是返回可執(zhí)行jar文件的啟動(dòng)類。MANIFEST.MF文件內(nèi)容
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文件為以上信息,可以看到兩個(gè)關(guān)鍵信息Main-Class和Start-Class。我們可以進(jìn)一步,程序的啟動(dòng)入口并不是我們SpringBoot中定義的main,而是JarLauncher#main,而再在其中利用反射調(diào)用定義好的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.ArchiveSpringboot抽象出來(lái)的統(tǒng)一訪問(wèn)資源的層JarFileArchivejar包文件的抽象ExplodedArchive文件目錄
JarFile的作用,每個(gè)JarFileArchive都會(huì)對(duì)應(yīng)一個(gè)JarFile。在構(gòu)造的時(shí)候會(huì)解析內(nèi)部結(jié)構(gòu),去獲取jar包里的各個(gè)文件或文件夾類。我們可以看一下該類的注釋。/* 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>
**/
自定義類加載機(jī)制
最基礎(chǔ):
Bootstrap ClassLoader(加載JDK的/lib目錄下的類)次基礎(chǔ):
Extension ClassLoader(加載JDK的/lib/ext目錄下的類)普通:
Application ClassLoader(程序自己classpath下的類)
ClassLoader加載,就不能讓高層的ClassLoader加載,這樣是為了范圍錯(cuò)誤的引入了非JDK下但是類名一樣的類。LaunchedURLClassLoader,該類繼承了java.net.URLClassLoader,重寫了java.lang.ClassLoader#loadClass(java.lang.String, boolean),然后我們?cè)偬接懰侨绾涡薷碾p親委派機(jī)制。LaunchedURLClassLoader的構(gòu)造方法。public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
the URLs from which to load classes and resources,即fat jar包依賴的所有類和資源,將該urls參數(shù)傳遞給父類java.net.URLClassLoader,由父類的java.net.URLClassLoader#findClass執(zhí)行查找類方法,該類的查找來(lái)源即構(gòu)造方法傳遞進(jìn)來(lái)的urls參數(shù)。//LaunchedURLClassLoader的實(shí)現(xiàn)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Handler.setUseFastConnectionExceptions(true);
try {
try {
//嘗試根據(jù)類名去定義類所在的包,即java.lang.Package,確保jar in jar里匹配的manifest能夠和關(guān)聯(lián) //的package關(guān)聯(lián)起來(lái)
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方法的作用實(shí)際上是預(yù)先過(guò)濾掉查找不到的包
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)實(shí)際上會(huì)回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循雙親委派機(jī)制進(jìn)行查找類,而Bootstrap ClassLoader和Extension ClassLoader將會(huì)查找不到fat jar依賴的類,最終會(huì)來(lái)到Application ClassLoader,調(diào)用java.net.URLClassLoader#findClass如何真正的啟動(dòng)
org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)反射調(diào)用邏輯比較簡(jiǎn)單,這里就不再分析,比較關(guān)鍵的一點(diǎn)是,在調(diào)用main方法之前,將當(dāng)前線程的上下文類加載器設(shè)置成LaunchedURLClassLoaderprotected 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();
// 構(gòu)造LaunchedURLClassLoader類加載器,這里使用了2個(gè)URL,分別對(duì)應(yīng)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個(gè)類都會(huì)在第二步本地查找中被找出(URLClassLoader的findClass方法)
classLoader.loadClass("org.springframework.boot.loader.JarLauncher");
classLoader.loadClass("org.springframework.boot.SpringApplication");
// 在第三步使用默認(rèn)的加載順序在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>
總結(jié)
對(duì)于源碼分析,這次的較大收獲則是不能一下子去追求弄懂源碼中的每一步代碼的邏輯,即便我知道該方法的作用。我們需要搞懂的是關(guān)鍵代碼,以及涉及到的知識(shí)點(diǎn)。
我從Maven的自定義插件開始進(jìn)行追蹤,鞏固了對(duì)Maven的知識(shí)點(diǎn),在這個(gè)過(guò)程中甚至了解到JDK對(duì)jar的讀取是有提供對(duì)應(yīng)的工具類。最后最重要的知識(shí)點(diǎn)則是自定義類加載器。整個(gè)代碼下來(lái)并不是說(shuō)代碼究竟有多優(yōu)秀,而是要學(xué)習(xí)他因何而優(yōu)秀。
來(lái)源:juejin.im/post/5d2d6812e51d45777b1a3e5a
精彩推薦:
保存好這個(gè)腳本,一鍵自動(dòng)部署 Redis 任意版本
從零開始搭建公司大型SaaS 平臺(tái)架構(gòu)技術(shù)棧,這套架構(gòu)絕了!
看看人家SpringBoot的全局異常處理多么優(yōu)雅...
IDEA 插件版的 Postman,接口調(diào)試簡(jiǎn)直太方便了!粉了。。。
