https://github.com/zq2599/blog_demos
內容:全部原創文章分類彙總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;html
在開發springboot應用時,經過java -jar命令啓動應用是經常使用的方式,今天就來一塊兒瞭解這個簡單操做背後的技術;java
開發一個springboot應用做爲本次研究的對象,對應的版本信息以下:git
接下來開發springboot應用,這個應用異常簡單:github
<?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>
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(); } }
mvn clean package -U -DskipTests
接下來就用這個springbootstarterdemo-0.0.1-SNAPSHOT.jar來分析jar文件可以獨立啓動的緣由;web
先要弄清楚java -jar命令作了什麼,在oracle官網找到了該命令的描述:
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.
spring
再次秀出我蹩腳的英文翻譯:docker
springbootstarterdemo-0.0.1-SNAPSHOT.jar是前面的springboot工程的構建結果,是個壓縮包,用常見的壓縮工具就能解壓,我這裏的環境是MacBook Pro,用unzip便可解壓;shell
解壓後有不少內容,咱們先關注manifest相關的,下圖紅框中就是manifest文件:
apache
打開上圖紅框中的文件,內容以下:springboot
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
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter demoApplication
動手以前先猜一下,我的以爲緣由應該以下:
先下載SpringBoot源碼,我下載的是2.3.1版本,地址:https://github.com/spring-projects/spring-boot/releases/tag/v2.3.1.RELEASE
JarLauncher所在的工程是spring-boot-loader,先弄明白JarLauncher的繼承關係,以下圖,可見JarLauncher繼承自ExecutableArchiveLauncher,而ExecutableArchiveLauncher的父類Launcher位於最頂層,是個抽象類:
java -jar執行的是JarLauncher的main方法,以下,會實例化一個JarLauncher對象,而後執行其launch方法,而且將全部入參都帶入:
public static void main(String[] args) throws Exception { new JarLauncher().launch(args); }
protected void launch(String[] args) throws Exception { // 將jar解壓後運行的方式叫作exploded mode // 若是是exploded mode,就不能支持經過URL加載jar // 若是不是exploded mode,就能夠經過URL加載jar if (!isExploded()) { // 若是容許經過URL加載jar,就在此註冊對應的處理類 JarFile.registerUrlProtocolHandler(); } // 建立classLoader ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); // jarmode是建立docker鏡像時用到的參數,使用該參數是爲了生成帶有多個layer信息的鏡像 // 這裏暫時不關注jarmode String jarMode = System.getProperty("jarmode"); //若是沒有jarmode參數,launchClass的值就來自getMainClass()返回 String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); launch(args, launchClass, classLoader); }
public ExecutableArchiveLauncher() { try { this.archive = createArchive(); this.classPathIndex = getClassPathIndex(this.archive); } catch (Exception ex) { throw new IllegalStateException(ex); } }
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)); }
@Override protected String getMainClass() throws Exception { // 對應的是JarFileArchive.getManifest方法, // 進去後發現對應的就是JarFile.getManifest方法, // JarFile.getManifest對應的就是META-INF/MANIFEST.MF文件的內容 Manifest manifest = this.archive.getManifest(); String mainClass = null; if (manifest != null) { // 對應的是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; }
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); }
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); // 經過反射執行main方法 mainMethod.invoke(null, new Object[] { this.args }); } }
終於,真相大白了;
最後儘量簡短作個小結,先看jar是如何產生的,以下圖,maven插件生成的jar文件中,有常見的class、jar,也有符合java規範的MANIFEST.MF文件,而且,還在MANIFEST.MF文件中額外生成了名爲Start-Class的配置,這裏面是咱們編寫的應用啓動類SpringbootstarterdemoApplication:
啓動類是JarLauncher,它是如何與MANIFEST.MF文件關聯的呢?從下圖能夠看出,最終是經過JarFile類的成員變量manifestSupplier關聯上的:
再來看看關鍵代碼的執行狀況,以下圖:
至此,SpringBoot的jar獨立運行的基本原理已經清楚,探究的過程當中,除了熟悉關鍵代碼流程,還對jar中的文件有了更多瞭解,若是您正在學習SpringBoot,但願本文能給您一些參考;
最後附上SpringBoot官方文檔,能夠看到Start-Class描述信息:
上述文檔明確提到:Start-Class定義的是實際的啓動類,此時的您應該對一切都瞭然於胸,產生本該如此的感慨;