Spring Boot jar可執(zhí)行原理的徹底分析
前言
文章篇幅較長,但是包含了SpringBoot 可執(zhí)行jar包從頭到尾的原理,請讀者耐心觀看。同時(shí)文章是基于SpringBoot-2.1.3進(jìn)行分析。涉及的知識點(diǎn)主要包括Maven的生命周期以及自定義插件,JDK提供關(guān)于jar包的工具類以及Springboot如何擴(kuò)展,最后是自定義類加載器。
spring-boot-maven-plugin
SpringBoot 的可執(zhí)行jar包又稱fat jar ,是包含所有第三方依賴的 jar 包,jar 包中嵌入了除 java 虛擬機(jī)以外的所有依賴,是一個(gè) 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
也就是說想要知道fat jar是如何生成的,就必須知道spring-boot-maven-plugin工作機(jī)制,而spring-boot-maven-plugin屬于自定義插件,因此我們又必須知道,Maven的自定義插件是如何工作的
Maven的自定義插件
Maven 擁有三套相互獨(dú)立的生命周期: clean、default 和 site, 而每個(gè)生命周期包含一些phase階段, 階段是有順序的, 并且后面的階段依賴于前面的階段。生命周期的階段phase與插件的目標(biāo)goal相互綁定,用以完成實(shí)際的構(gòu)建任務(wù)。
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>
repackage目標(biāo)對應(yīng)的將執(zhí)行到org.springframework.boot.maven.RepackageMojo#execute,該方法的主要邏輯是調(diào)用了org.springframework.boot.maven.RepackageMojo#repackage
private void repackage() throws MojoExecutionException { //獲取使用maven-jar-plugin生成的jar,最終的命名將加上.orignal后綴 Artifact source = getSourceArtifact(); //最終文件,即Fat jar File target = getTargetFile(); //獲取重新打包器,將重新打包成可執(zhí)行jar文件 Repackager repackager = getRepackager(source.getFile()); //查找并過濾項(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啟動腳本 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()); }
我們關(guān)心一下org.springframework.boot.maven.RepackageMojo#getRepackager這個(gè)方法,知道Repackager是如何生成的,也就大致能夠推測出內(nèi)在的打包邏輯。
private Repackager getRepackager(File source) { Repackager repackager = new Repackager(source, this.layoutFactory); repackager.addMainClassTimeoutWarningListener( new LoggingMainClassTimeoutWarningListener()); //設(shè)置main class的名稱,如果不指定的話則會查找第一個(gè)包含main方法的類,repacke最后將會設(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; } }
layout我們可以將之翻譯為文件布局,或者目錄布局,代碼一看清晰明了,同時(shí)我們需要關(guān)注,也是下一個(gè)重點(diǎn)關(guān)注對象org.springframework.boot.loader.JarLauncher,從名字推斷,這很可能是返回可執(zhí)行jar文件的啟動類。
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
repackager生成的MANIFEST.MF文件為以上信息,可以看到兩個(gè)關(guān)鍵信息Main-Class和Start-Class。我們可以進(jìn)一步,程序的啟動入口并不是我們SpringBoot中定義的main,而是JarLauncher#main,而再在其中利用反射調(diào)用定義好的Start-Class的main方法
JarLauncher
重點(diǎn)類介紹
- 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抽象出來的統(tǒng)一訪問資源的層
- JarFileArchivejar包文件的抽象
- ExplodedArchive文件目錄
這里重點(diǎn)描述一下JarFile的作用,每個(gè)JarFileArchive都會對應(yīng)一個(gè)JarFile。在構(gòu)造的時(shí)候會解析內(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>
jar里的資源分隔符是!/,在JDK提供的JarFile URL只支持一個(gè)'!/‘,而Spring boot擴(kuò)展了這個(gè)協(xié)議,讓它支持多個(gè)'!/‘,就可以表示jar in jar、jar in directory、fat jar的資源了。
自定義類加載機(jī)制
- 最基礎(chǔ):Bootstrap ClassLoader(加載JDK的/lib目錄下的類)
- 次基礎(chǔ):Extension ClassLoader(加載JDK的/lib/ext目錄下的類)
- 普通:Application ClassLoader(程序自己classpath下的類)
首先需要關(guān)注雙親委派機(jī)制很重要的一點(diǎn)是,如果一個(gè)類可以被委派最基礎(chǔ)的ClassLoader加載,就不能讓高層的ClassLoader加載,這樣是為了范圍錯(cuò)誤的引入了非JDK下但是類名一樣的類。其二,如果在這個(gè)機(jī)制下,由于fat jar中依賴的各個(gè)第三方j(luò)ar文件,并不在程序自己classpath下,也就是說,如果我們采用雙親委派機(jī)制的話,根本獲取不到我們所依賴的jar包,因此我們需要修改雙親委派機(jī)制的查找class的方法,自定義類加載機(jī)制。
先簡單的介紹Springboot2中LaunchedURLClassLoader,該類繼承了java.net.URLClassLoader,重寫了java.lang.ClassLoader#loadClass(java.lang.String, boolean),然后我們再探討他是如何修改雙親委派機(jī)制。
在上面我們講到Spring boot支持多個(gè)'!/‘以表示多個(gè)jar,而我們的問題在于,如何解決查找到這多個(gè)jar包。我們看一下LaunchedURLClassLoader的構(gòu)造方法。
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); }
urls注釋解釋道the URLs from which to load classes and resources,即fat jar包依賴的所有類和資源,將該urls參數(shù)傳遞給父類java.net.URLClassLoader,由父類的java.net.URLClassLoader#findClass執(zhí)行查找類方法,該類的查找來源即構(gòu)造方法傳遞進(jìn)來的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)起來 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ù)先過濾掉查找不到的包 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í)際上會回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循雙親委派機(jī)制進(jìn)行查找類,而Bootstrap ClassLoader和Extension ClassLoader將會查找不到fat jar依賴的類,最終會來到Application ClassLoader,調(diào)用java.net.URLClassLoader#findClass
如何真正的啟動
Springboot2和Springboot1的最大區(qū)別在于,Springboo1會新起一個(gè)線程,來執(zhí)行相應(yīng)的反射調(diào)用邏輯,而SpringBoot2則去掉了構(gòu)建新的線程這一步。方法是org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)反射調(diào)用邏輯比較簡單,這里就不再分析,比較關(guān)鍵的一點(diǎn)是,在調(diào)用main方法之前,將當(dāng)前線程的上下文類加載器設(shè)置成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(); // 構(gòu)造LaunchedURLClassLoader類加載器,這里使用了2個(gè)URL,分別對應(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è)類都會在第二步本地查找中被找出(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); }
<!-- 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>
總結(jié)
對于源碼分析,這次的較大收獲則是不能一下子去追求弄懂源碼中的每一步代碼的邏輯,即便我知道該方法的作用。我們需要搞懂的是關(guān)鍵代碼,以及涉及到的知識點(diǎn)。我從Maven的自定義插件開始進(jìn)行追蹤,鞏固了對Maven的知識點(diǎn),在這個(gè)過程中甚至了解到JDK對jar的讀取是有提供對應(yīng)的工具類。最后最重要的知識點(diǎn)則是自定義類加載器。整個(gè)代碼下來并不是說代碼究竟有多優(yōu)秀,而是要學(xué)習(xí)他因何而優(yōu)秀。
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對腳本之家的支持。
相關(guān)文章
SpringBoot整合easyExcel實(shí)現(xiàn)CSV格式文件的導(dǎo)入導(dǎo)出
這篇文章主要為大家詳細(xì)介紹了SpringBoot整合easyExcel實(shí)現(xiàn)CSV格式文件的導(dǎo)入導(dǎo)出,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴可以參考下2024-02-02Java中關(guān)于控制臺讀取數(shù)字或字符串的方法
下面小編就為大家?guī)硪黄狫ava中關(guān)于控制臺讀取數(shù)字或字符串的方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-10-10Java中不得不知的Collection接口與Iterator迭代器
這篇文章主要介紹了Java中的Collection接口與Iterator迭代器,文中有詳細(xì)的代碼示例供大家參考,對我們的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-06-06Netty啟動流程服務(wù)端channel初始化源碼分析
這篇文章主要為大家介紹了Netty啟動流程服務(wù)端channel初始化源碼分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03