springboot的jar能獨立運行的原因解析
歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內(nèi)容:所有原創(chuàng)文章分類匯總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;
歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內(nèi)容:所有原創(chuàng)文章分類匯總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;
能獨立運行的jar文件
在開發(fā)springboot應(yīng)用時,通過java -jar命令啟動應(yīng)用是常用的方式,今天就來一起了解這個簡單操作背后的技術(shù);
開發(fā)demo
開發(fā)一個springboot應(yīng)用作為本次研究的對象,對應(yīng)的版本信息如下:
- JDK:1.8.0_211
- springboot:2.3.1.RELEASE
- maven:3.6.0
接下來開發(fā)springboot應(yīng)用,這個應(yīng)用非常簡單:
springboot應(yīng)用名為springbootstarterdemo,pom.xml文件內(nèi)容:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.bolingcavalry</groupId> <artifactId>springbootstarterdemo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springbootstarterdemo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
只有一個java類,里面有個http接口:
package com.bolingcavalry.springbootstarterdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Date; @SpringBootApplication @RestController public class SpringbootstarterdemoApplication { public static void main(String[] args) { SpringApplication.run(SpringbootstarterdemoApplication.class, args); } @RequestMapping(value = "/hello") public String hello(){ return "hello " + new Date(); } }
編碼完成,在pom.xml所在目錄執(zhí)行命令
mvn clean package -U -DskipTests
構(gòu)建成功后,在target目錄下得到文件springbootstarterdemo-0.0.1-SNAPSHOT.jar就是這個springbootstarterdemo-0.0.1-SNAPSHOT.jar,此時執(zhí)行java -jar springbootstarterdemo-0.0.1-SNAPSHOT.jar就能啟動應(yīng)用,如下圖:
接下來就用這個springbootstarterdemo-0.0.1-SNAPSHOT.jar來分析jar文件能夠獨立啟動的原因;
java -jar做了什么
先要弄清楚java -jar命令做了什么,在oracle官網(wǎng)找到了該命令的描述:
If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. The startup class must be indicated by the Main-Class manifest header in its source code.
再次秀出我蹩腳的英文翻譯:
- 使用-jar參數(shù)時,后面的參數(shù)是的jar文件名(本例中是springbootstarterdemo-0.0.1-SNAPSHOT.jar);
- 該jar文件中包含的是class和資源文件;
- 在manifest文件中有Main-Class的定義;
- Main-Class的源碼中指定了整個應(yīng)用的啟動類;(in its source code)
小結(jié)一下:
java -jar會去找jar中的manifest文件,在那里面找到真正的啟動類;
探查springbootstarterdemo-0.0.1-SNAPSHOT.jarspringbootstarterdemo-0.0.1-SNAPSHOT.jar是前面的springboot工程的構(gòu)建結(jié)果,是個壓縮包,用常見的壓縮工具就能解壓,我這里的環(huán)境是MacBook Pro,用unzip即可解壓;解壓后有很多內(nèi)容,我們先關(guān)注manifest相關(guān)的,下圖紅框中就是manifest文件:
打開上圖紅框中的文件,內(nèi)容如下:
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx Implementation-Title: springbootstarterdemo Implementation-Version: 0.0.1-SNAPSHOT Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter demoApplication Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Build-Jdk-Spec: 1.8 Spring-Boot-Version: 2.3.1.RELEASE Created-By: Maven Jar Plugin 3.2.0 Implementation-Vendor: Pivotal Software, Inc. Main-Class: org.springframework.boot.loader.JarLauncher
4.在上述內(nèi)容可見Main-Class的值org.springframework.boot.loader.JarLauncher,這個和前面的java官方文檔對應(yīng)上了,正是這個JarLauncher類的代碼中指定了真正的啟動類;
疑惑出現(xiàn)
1.在MANIFEST.MF文件中有這么一行內(nèi)容:
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter demoApplication
2.前面的java官方文檔中,只提到過Main-Class ,并沒有提到Start-Class;
3.Start-Class的值是SpringbootstarterdemoApplication,這是我們的java代碼中的唯一類,也只真正的應(yīng)用啟動類;
4.所以問題就來了:理論上看,執(zhí)行java -jar命令時JarLauncher類會被執(zhí)行,但實際上是SpringbootstarterdemoApplication被執(zhí)行了,這其中發(fā)生了什么呢?
猜測
動手之前先猜一下,個人覺得原因應(yīng)該如下:
- java -jar命令會啟動JarLauncher;
- Start-Class是給JarLauncher用的;
- JarLauncher根據(jù)Start-Class找到了SpringbootstarterdemoApplication,然后執(zhí)行它;
分析JarLauncher先下載SpringBoot源碼,我下載的是2.3.1版本,地址:https://github.com/spring-projects/spring-boot/releases/tag/v2.3.1.RELEASEJarLauncher所在的工程是spring-boot-loader,先弄明白JarLauncher的繼承關(guān)系,如下圖,可見JarLauncher繼承自ExecutableArchiveLauncher,而ExecutableArchiveLauncher的父類Launcher位于最頂層,是個抽象類:
3. java -jar執(zhí)行的是JarLauncher的main方法,如下,會實例化一個JarLauncher對象,然后執(zhí)行其launch方法,并且將所有入?yún)⒍紟耄?/p>
public static void main(String[] args) throws Exception { new JarLauncher().launch(args); }
4.上面的launch方法在父類Launcher中:
protected void launch(String[] args) throws Exception { // 將jar解壓后運行的方式叫做exploded mode // 如果是exploded mode,就不能支持通過URL加載jar // 如果不是exploded mode,就可以通過URL加載jar if (!isExploded()) { // 如果允許通過URL加載jar,就在此注冊對應(yīng)的處理類 JarFile.registerUrlProtocolHandler(); } // 創(chuàng)建classLoader ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); // jarmode是創(chuàng)建docker鏡像時用到的參數(shù),使用該參數(shù)是為了生成帶有多個layer信息的鏡像 // 這里暫時不關(guān)注jarmode String jarMode = System.getProperty("jarmode"); //如果沒有jarmode參數(shù),launchClass的值就來自getMainClass()返回 String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); launch(args, launchClass, classLoader); }
5.可見要重點關(guān)注的是getMainClass()方法,在看這個方法之前,我們先去關(guān)注一個重要的成員變量archive,是JarLauncher的父類ExecutableArchiveLauncher的archive,如下可見,該變量又來自方法createArchive:
public ExecutableArchiveLauncher() { try { this.archive = createArchive(); this.classPathIndex = getClassPathIndex(this.archive); } catch (Exception ex) { throw new IllegalStateException(ex); } }
6.方法來自Launcher.createArchive,如下所示,可見成員變量archive實際上是個JarFileArchive對象:
protected final Archive createArchive() throws Exception { 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); } return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); }
7.現(xiàn)在回到getMainClass()方法,可見this.archive.getManifest方法返回的是META-INF/MANIFEST.MF文件的內(nèi)容,然后getValue(START_CLASS_ATTRIBUTE)方法實際上就是從META-INF/MANIFEST.MF中取得了Start-Class的屬性:
@Override protected String getMainClass() throws Exception { // 對應(yīng)的是JarFileArchive.getManifest方法, // 進去后發(fā)現(xiàn)對應(yīng)的就是JarFile.getManifest方法, // JarFile.getManifest對應(yīng)的就是META-INF/MANIFEST.MF文件的內(nèi)容 Manifest manifest = this.archive.getManifest(); String mainClass = null; if (manifest != null) { // 對應(yīng)的是META-INF/MANIFEST.MF文件中的Start-Class的屬性 mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE); } if (mainClass == null) { throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); } return mainClass; }
8.從上述分析可知:getMainClass()方法返回的是META-INF/MANIFEST.MF中取得了Start-Class的屬性com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication,再次回到launch方法中,可見最終運行的代碼是launch(args, launchClass, classLoader),它的launchClass參數(shù)就是com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication:
protected void launch(String[] args) throws Exception { if (!isExploded()) { JarFile.registerUrlProtocolHandler(); } ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); String jarMode = System.getProperty("jarmode"); // 這里的launchClass等于"com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication" String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); // 這里就是啟動SpringbootstarterdemoApplication的地方 launch(args, launchClass, classLoader); }
9.展開launch(args, launchClass, classLoader),最終查到了MainMethodRunner類:
public class MainMethodRunner { private final String mainClassName; private final String[] args; /** * Create a new {@link MainMethodRunner} instance. * @param mainClass the main class * @param args incoming arguments */ public MainMethodRunner(String mainClass, String[] args) { // mainClassName被賦值為"com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication" this.mainClassName = mainClass; this.args = (args != null) ? args.clone() : null; } public void run() throws Exception { // 得到SpringbootstarterdemoApplication的Class對象 Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader()); // 得到SpringbootstarterdemoApplication的main方法對象 Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); mainMethod.setAccessible(true); // 通過反射執(zhí)行main方法 mainMethod.invoke(null, new Object[] { this.args }); } }
終于,真相大白了;
小結(jié)
最后盡可能簡短做個小結(jié),先看jar是如何產(chǎn)生的,如下圖,maven插件生成的jar文件中,有常見的class、jar,也有符合java規(guī)范的MANIFEST.MF文件,并且,還在MANIFEST.MF文件中額外生成了名為Start-Class的配置,這里面是我們編寫的應(yīng)用啟動類SpringbootstarterdemoApplication:
啟動類是JarLauncher,它是如何與MANIFEST.MF文件關(guān)聯(lián)的呢?從下圖可以看出,最終是通過JarFile類的成員變量manifestSupplier關(guān)聯(lián)上的:
再來看看關(guān)鍵代碼的執(zhí)行情況,如下圖:
至此,SpringBoot的jar獨立運行的基本原理已經(jīng)清楚,探究的過程中,除了熟悉關(guān)鍵代碼流程,還對jar中的文件有了更多了解,如果您正在學(xué)習(xí)SpringBoot,希望本文能給您一些參考;
官方文檔最后附上SpringBoot官方文檔,可以看到Start-Class描述信息:
上述文檔明確提到:Start-Class定義的是實際的啟動類,此時的您應(yīng)該對一切都了然于胸,產(chǎn)生本該如此的感慨;
到此這篇關(guān)于springboot的jar為何能獨立運行的文章就介紹到這了,更多相關(guān)springboot的jar為何能獨立運行內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot項目 文件上傳臨時目標(biāo)被刪除異常的處理方案
這篇文章主要介紹了SpringBoot項目 文件上傳臨時目標(biāo)被刪除異常的處理方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Java調(diào)用WebService服務(wù)的三種方式總結(jié)
雖然WebService這個框架已經(jīng)過時,但是有些公司還在使用,在調(diào)用他們的服務(wù)的時候就不得不面對各種問題,本篇文章總結(jié)了最近我調(diào)用?WebService的心路歷程,3種方式可以分別嘗試,需要的朋友可以參考下2023-08-08深入理解java內(nèi)置鎖(synchronized)和顯式鎖(ReentrantLock)
這篇文章主要介紹了Java多線程之內(nèi)置鎖(synchronized)和顯式鎖(ReentrantLock)的深入理解新的和用法,具有一定參考價值,需要的朋友可以了解下。2017-11-11Docker 部署 SpringBoot 項目整合 Redis 鏡像做訪問計數(shù)示例代碼
這篇文章主要介紹了Docker 部署 SpringBoot 項目整合 Redis 鏡像做訪問計數(shù)Demo,本文給大家介紹的非常詳細,具有參考借鑒價值,需要的朋友可以參考下2018-01-01Java中Collections.emptyList()的注意事項
這篇文章主要給大家介紹了關(guān)于Java中Collections.emptyList()的注意事項,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03Tomcat數(shù)據(jù)源配置方法_JBuilder中
今天幫一同事配置一個數(shù)據(jù)源,采用tomcat5.5.9,本來是個很簡單的事,以前也配過,但由于很長時間沒用過容器提供的數(shù)據(jù)源了(IOC用慣了),也只記的個大概了,所以剛開始一配就出錯了,google了一下,有很多資料,照著試試卻都不好使(到不是別人說的不對,只是大家用的版本不同)。2008-10-10Java BeanUtils.copyProperties的詳解
這篇文章主要介紹了Java BeanUtils.copyProperties的詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-08-08使用Springboot整合GridFS實現(xiàn)文件操作
這篇文章主要介紹了使用Springboot整合GridFS實現(xiàn)文件操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10