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應用時,通過<font color="blue">java -jar</font>命令啟動應用是常用的方式,今天就來一起了解這個簡單操作背后的技術;

開發(fā)demo

開發(fā)一個springboot應用作為本次研究的對象,對應的版本信息如下:

  • JDK:1.8.0_211
  • springboot:2.3.1.RELEASE
  • maven:3.6.0

接下來開發(fā)springboot應用,這個應用非常簡單:

  1. springboot應用名為<font color="blue">springbootstarterdemo</font>,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>
  1. 只有一個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();
    }
}
  1. 編碼完成,在pom.xml所在目錄執(zhí)行命令
mvn clean package -U -DskipTests
  1. 構(gòu)建成功后,在target目錄下得到文件<font color="blue">springbootstarterdemo-0.0.1-SNAPSHOT.jar</font>
  2. 就是這個<font color="blue">springbootstarterdemo-0.0.1-SNAPSHOT.jar</font>,此時執(zhí)行<font color="blue">java -jar springbootstarterdemo-0.0.1-SNAPSHOT.jar</font>就能啟動應用,如下圖:


    在這里插入圖片描述

    接下來就用這個<font color="blue">springbootstarterdemo-0.0.1-SNAPSHOT.jar</font>來分析jar文件能夠獨立啟動的原因;

java -jar做了什么

  • 先要弄清楚<font color="blue">java -jar</font>命令做了什么,在oracle官網(wǎng)找到了該命令的描述:
    <font color="blue">
    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.
    </font>

  • 再次秀出我蹩腳的英文翻譯:

  1. 使用<font color="blue">-jar</font>參數(shù)時,后面的參數(shù)是的jar文件名(本例中是springbootstarterdemo-0.0.1-SNAPSHOT.jar);
  2. 該jar文件中包含的是class和資源文件;
  3. 在manifest文件中有<font color="blue">Main-Class</font>的定義;
  4. <font color="blue">Main-Class</font>的源碼中指定了整個應用的啟動類;(in its source code)

小結(jié)一下:
<font color="blue">java -jar</font>會去找jar中的manifest文件,在那里面找到真正的啟動類;

探查springbootstarterdemo-0.0.1-SNAPSHOT.jar

  1. <font color="blue">springbootstarterdemo-0.0.1-SNAPSHOT.jar</font>是前面的springboot工程的構(gòu)建結(jié)果,是個壓縮包,用常見的壓縮工具就能解壓,我這里的環(huán)境是MacBook Pro,用unzip即可解壓;
  2. 解壓后有很多內(nèi)容,我們先關注manifest相關的,下圖紅框中就是manifest文件:


    在這里插入圖片描述
  3. 打開上圖紅框中的文件,內(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
  1. 在上述內(nèi)容可見Main-Class的值<font color="blue">org.springframework.boot.loader.JarLauncher</font>,這個和前面的java官方文檔對應上了,正是這個JarLauncher類的代碼中指定了真正的啟動類;

疑惑出現(xiàn)

  1. 在MANIFEST.MF文件中有這么一行內(nèi)容:
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
 demoApplication
  1. 前面的java官方文檔中,只提到過<font color="blue">Main-Class </font>,并沒有提到<font color="blue">Start-Class</font>;
  2. Start-Class的值是SpringbootstarterdemoApplication,這是我們的java代碼中的唯一類,也只真正的應用啟動類;
  3. 所以問題就來了:理論上看,執(zhí)行java -jar命令時JarLauncher類會被執(zhí)行,但實際上是SpringbootstarterdemoApplication被執(zhí)行了,這其中發(fā)生了什么呢?

猜測

動手之前先猜一下,個人覺得原因應該如下:

  1. java -jar命令會啟動JarLauncher;
  2. <font color="blue">Start-Class</font>是給JarLauncher用的;
  3. JarLauncher根據(jù)<font color="blue">Start-Class</font>找到了SpringbootstarterdemoApplication,然后執(zhí)行它;

分析JarLauncher

  1. 先下載SpringBoot源碼,我下載的是2.3.1版本,地址:https://github.com/spring-projects/spring-boot/releases/tag/v2.3.1.RELEASE
  2. JarLauncher所在的工程是spring-boot-loader,先弄明白JarLauncher的繼承關系,如下圖,可見JarLauncher繼承自ExecutableArchiveLauncher,而ExecutableArchiveLauncher的父類Launcher位于最頂層,是個抽象類:
在這里插入圖片描述
  1. java -jar執(zhí)行的是JarLauncher的main方法,如下,會實例化一個JarLauncher對象,然后執(zhí)行其launch方法,并且將所有入?yún)⒍紟耄?/li>
public static void main(String[] args) throws Exception {
    new JarLauncher().launch(args);
}
  1. 上面的launch方法在父類Launcher中:
protected void launch(String[] args) throws Exception {
    // 將jar解壓后運行的方式叫做exploded mode
    // 如果是exploded mode,就不能支持通過URL加載jar
    // 如果不是exploded mode,就可以通過URL加載jar
    if (!isExploded()) {
        // 如果允許通過URL加載jar,就在此注冊對應的處理類
        JarFile.registerUrlProtocolHandler();
    }
    // 創(chuàng)建classLoader
    ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
    // jarmode是創(chuàng)建docker鏡像時用到的參數(shù),使用該參數(shù)是為了生成帶有多個layer信息的鏡像
    // 這里暫時不關注jarmode
    String jarMode = System.getProperty("jarmode");
    //如果沒有jarmode參數(shù),launchClass的值就來自getMainClass()返回
    String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
    launch(args, launchClass, classLoader);
}
  1. 可見要重點關注的是<font color="blue">getMainClass()</font>方法,在看這個方法之前,我們先去關注一個重要的成員變量<font color="blue">archive</font>,是JarLauncher的父類ExecutableArchiveLauncher的archive,如下可見,該變量又來自方法<font color="blue">createArchive</font>:
public ExecutableArchiveLauncher() {
        try {
            this.archive = createArchive();
            this.classPathIndex = getClassPathIndex(this.archive);
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
  1. 方法來自Launcher.createArchive,如下所示,可見成員變量<font color="blue">archive</font>實際上是個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));
    }
  1. 現(xiàn)在回到getMainClass()方法,可見<font color="blue">this.archive.getManifest</font>方法返回的是<font color="red">META-INF/MANIFEST.MF</font>文件的內(nèi)容,然后getValue(START_CLASS_ATTRIBUTE)方法實際上就是從META-INF/MANIFEST.MF中取得了Start-Class的屬性:
@Override
    protected String getMainClass() throws Exception {
        // 對應的是JarFileArchive.getManifest方法,
        // 進去后發(fā)現(xiàn)對應的就是JarFile.getManifest方法,
        // JarFile.getManifest對應的就是META-INF/MANIFEST.MF文件的內(nèi)容
        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;
    }
  1. 從上述分析可知:getMainClass()方法返回的是META-INF/MANIFEST.MF中取得了<font color="red">Start-Class</font>的屬性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);
    }
  1. 展開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文件中額外生成了名為<font color="blue">Start-Class</font>的配置,這里面是我們編寫的應用啟動類<font color="blue">SpringbootstarterdemoApplication</font>:

在這里插入圖片描述

啟動類是<font color="blue">JarLauncher</font>,它是如何與MANIFEST.MF文件關聯(lián)的呢?從下圖可以看出,最終是通過JarFile類的成員變量<font color="blue">manifestSupplier</font>關聯(lián)上的:

在這里插入圖片描述

再來看看關鍵代碼的執(zhí)行情況,如下圖:

在這里插入圖片描述

至此,SpringBoot的jar獨立運行的基本原理已經(jīng)清楚,探究的過程中,除了熟悉關鍵代碼流程,還對jar中的文件有了更多了解,如果您正在學習SpringBoot,希望本文能給您一些參考;

官方文檔

  1. 最后附上SpringBoot官方文檔,可以看到<font color="blue">Start-Class</font>描述信息:
在這里插入圖片描述
  1. 上述文檔明確提到:Start-Class定義的是實際的啟動類,此時的您應該對一切都了然于胸,產(chǎn)生本該如此的感慨;

你不孤單,欣宸原創(chuàng)一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 數(shù)據(jù)庫+中間件系列
  6. DevOps系列

歡迎關注公眾號:程序員欣宸

微信搜索「程序員欣宸」,我是欣宸,期待與您一同暢游Java世界...
https://github.com/zq2599/blog_demos

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容