文章篇幅較長,可是包含了SpringBoot 可執行jar包從頭至尾的原理,請讀者耐心觀看。同時文章是基於
SpringBoot-2.1.3
進行分析。涉及的知識點主要包括Maven的生命週期以及自定義插件,JDK提供關於jar包的工具類以及Springboot如何擴展,最後是自定義類加載器。java
SpringBoot 的可執行jar包又稱fat jar
,是包含全部第三方依賴的 jar 包,jar 包中嵌入了除 java 虛擬機之外的全部依賴,是一個 all-in-one jar 包。普通插件maven-jar-plugin
生成的包和spring-boot-maven-plugin
生成的包之間的直接區別,是fat jar
中主要增長了兩部分,第一部分是lib目錄,存放的是Maven依賴的jar包文件,第二部分是spring boot loader
相關的類。git
fat jar 目錄結構
├─BOOT-INF
│ ├─classes
│ └─lib
├─META-INF
│ ├─maven
│ ├─app.properties
│ ├─MANIFEST.MF
└─org
└─springframework
└─boot
└─loader
├─archive
├─data
├─jar
└─util
複製代碼
也就是說想要知道fat jar
是如何生成的,就必須知道spring-boot-maven-plugin
工做機制,而spring-boot-maven-plugin
屬於自定義插件,所以咱們又必須知道,Maven的自定義插件是如何工做的github
Maven 擁有三套相互獨立的生命週期: clean、default 和 site, 而每一個生命週期包含一些phase階段, 階段是有順序的, 而且後面的階段依賴於前面的階段。生命週期的階段phase與插件的目標goal相互綁定,用以完成實際的構建任務。web
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
複製代碼
repackage
目標對應的將執行到org.springframework.boot.maven.RepackageMojo#execute
,該方法的主要邏輯是調用了org.springframework.boot.maven.RepackageMojo#repackage
spring
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
是如何生成的,也就大體可以推測出內在的打包邏輯。json
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;
}
}
複製代碼
layout
咱們能夠將之翻譯爲文件佈局,或者目錄佈局,代碼一看清晰明瞭,同時咱們須要關注,也是下一個重點關注對象org.springframework.boot.loader.JarLauncher
,從名字推斷,這極可能是返回可執行jar
文件的啓動類。springboot
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
複製代碼
repackager
生成的MANIFEST.MF文件爲以上信息,能夠看到兩個關鍵信息Main-Class
和Start-Class
。咱們能夠進一步,程序的啓動入口並非咱們SpringBoot中定義的main
,而是JarLauncher#main
,而再在其中利用反射調用定義好的Start-Class
的main
方法bash
java.util.jar.JarFile
JDK工具類提供的讀取jar
文件org.springframework.boot.loader.jar.JarFile
Springboot-loader 繼承JDK提供JarFile
類java.util.jar.JarEntry
DK工具類提供的``jar```文件條目org.springframework.boot.loader.jar.JarEntry
Springboot-loader 繼承JDK提供JarEntry
類org.springframework.boot.loader.archive.Archive
Springboot抽象出來的統一訪問資源的層
JarFileArchive
jar包文件的抽象ExplodedArchive
文件目錄 這裏重點描述一下JarFile
的做用,每一個JarFileArchive
都會對應一個JarFile
。在構造的時候會解析內部結構,去獲取jar
包裏的各個文件或文件夾類。咱們能夠看一下該類的註釋。app
/* 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>
複製代碼
jar
裏的資源分隔符是!/
,在JDK提供的JarFile
URL只支持一個’!/‘,而Spring boot擴展了這個協議,讓它支持多個’!/‘,就能夠表示jar in jar、jar in directory、fat jar的資源了。maven
首先須要關注雙親委派機制很重要的一點是,若是一個類能夠被委派最基礎的ClassLoader加載,就不能讓高層的ClassLoader加載,這樣是爲了範圍錯誤的引入了非JDK下可是類名同樣的類。其二,若是在這個機制下,因爲fat jar
中依賴的各個第三方jar
文件,並不在程序本身classpath
下,也就是說,若是咱們採用雙親委派機制的話,根本獲取不到咱們所依賴的jar包,所以咱們須要修改雙親委派機制的查找class的方法,自定義類加載機制。
先簡單的介紹Springboot2中LaunchedURLClassLoader
,該類繼承了java.net.URLClassLoader
,重寫了java.lang.ClassLoader#loadClass(java.lang.String, boolean)
,而後咱們再探討他是如何修改雙親委派機制。
在上面咱們講到Spring boot支持多個’!/‘以表示多個jar,而咱們的問題在於,如何解決查找到這多個jar包。咱們看一下LaunchedURLClassLoader
的構造方法。
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
複製代碼
urls
註釋解釋道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
Springboot2和Springboot1的最大區別在於,Springboo1會新起一個線程,來執行相應的反射調用邏輯,而SpringBoot2則去掉了構建新的線程這一步。方法是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();
}
複製代碼
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);
}
複製代碼
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-loader -->
<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的讀取是有提供對應的工具類。最後最重要的知識點則是自定義類加載器。整個代碼下來並非說代碼究竟有多優秀,而是要學習他因何而優秀。
做者:plz叫我紅領巾
本博客歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。碼字不易,您的點贊是我寫做的最大動力。