聊一聊 SpringBoot 中 FatJar 啓動原理

以前有寫過一篇文章來介紹 JAR 文件和 MENIFEST.MF 文件,詳見:聊一聊 JAR 文件和 MANIFEST.MF,在這篇文章中介紹了 JAR 文件的內部結構。本篇將繼續延續前面的節奏,來介紹下,在 SpringBoot 中,是如何將一個 FatJar 運行起來的。html

FatJar 解壓以後的文件目錄

Spring 官網 或者經過 Idea 建立一個新的 SpringBoot 工程,方便起見,建議什麼依賴都不加,默認帶入的空的 SpringBoot 工程便可。java

經過 maven 命令進行打包,打包成功以後獲得的構建產物截圖以下:spring

在前面的文章中有提到,jar 包是zip 包的一種變種,所以也能夠經過 unzip 來解壓windows

unzip -q guides-for-jarlaunch-0.0.1-SNAPSHOT.jar -d mock
複製代碼

解壓的 mock 目錄,使用 tree 指令,看到整個解壓以後的 FatJar 的目錄結構以下(部分省略):api

.
├── BOOT-INF
│   ├── classes
│   │   ├── application.properties  # 用戶-配置文件
│   │   └── com
│   │       └── glmapper
│   │           └── bridge
│   │               └── boot
│   │                   └── BootStrap.class  # 用戶-啓動類
│   └── lib
│       ├── jakarta.annotation-api-1.3.5.jar
│       ├── jul-to-slf4j-1.7.28.jar
│       ├── log4j-xxx.jar # 表示 log4j 相關的依賴簡寫
│       ├── logback-xxx.jar # 表示 logback 相關的依賴簡寫
│       ├── slf4j-api-1.7.28.jar
│       ├── snakeyaml-1.25.jar
│       ├── spring-xxx.jar   # 表示 spring 相關的依賴簡寫
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.glmapper.bridge.boot
│           └── guides-for-jarlaunch
│               ├── pom.properties
│               └── pom.xml
└── org
    └── springframework
        └── boot
            └── loader
                ├── ExecutableArchiveLauncher.class
                ├── JarLauncher.class
                ├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
                ├── LaunchedURLClassLoader.class
                ├── Launcher.class
                ├── MainMethodRunner.class
                ├── PropertiesLauncher$1.class
                ├── PropertiesLauncher$ArchiveEntryFilter.class
                ├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
                ├── PropertiesLauncher.class
                ├── WarLauncher.class
                ├── archive
                │   ├── # 省略
                ├── data
                │   ├── # 省略
                ├── jar
                │   ├── # 省略
                └── util
                    └── SystemPropertyUtils.class

複製代碼

簡單來看,FatJar 解壓以後包括三個文件夾:springboot

├── BOOT-INF # 存放的是業務相關的,包括業務開發的類和配置文件,以及依賴的jar
│   ├── classes
│   └── lib
├── META-INF # 包括 MANIFEST.MF 描述文件和 maven 的構建信息
│   ├── MANIFEST.MF
│   └── maven
└── org # SpringBoot 相關的類
    └── springframework
複製代碼

咱們平時在 debug SpringBoot 工程的啓動流程時,通常都是從 SpringApplication#run 方法開始bash

@SpringBootApplication
public class BootStrap {
    public static void main(String[] args) {
        // 入口
        SpringApplication.run(BootStrap.class,args);
    }
}
複製代碼

對於 java 程序來講,咱們知道啓動入口必須有 main 函數,這裏看起來是符合條件的,可是有一點就是,經過 java 指令執行一個帶有 main 函數的類時,是不須要有 -jar 參數的,好比新建一個 BootStrap.java 文件,內容爲:oracle

public class BootStrap {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}
複製代碼

經過 javac 編譯此文件:app

javac BootStrap.java
複製代碼

而後就能夠獲得編譯以後的 .class 文件 BootStrap.class ,此時能夠經過 java 指令直接執行:maven

java BootStrap  # 輸出 Hello World
複製代碼

那麼對於 java -jar 呢?這個其實在 java 的官方文檔 中是有明確描述的:

  • -jar filename

Executes a program encapsulated in a JAR file. The filename argument is the name of a JAR file with a manifest that contains a line in the form Main-Class:classname that defines the class with the public static void main(String[] args) method that serves as your application's starting point.

When you use the -jar option, the specified JAR file is the source of all user classes, and other class path settings are ignored.

簡單說就是,java -jar 命令引導的具體啓動類必須配置在 MANIFEST.MF 資源的 Main-Class 屬性中。

那回過頭再去看下以前打包好、解壓以後的文件目錄,找到 /META-INF/MANIFEST.MF 文件,看下元數據:

Manifest-Version: 1.0
Implementation-Title: guides-for-jarlaunch
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.glmapper.bridge.boot.BootStrap
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.0.RELEASE
Created-By: Maven Archiver 3.4.0
# Main-Class 在這裏,指向的是 JarLauncher
Main-Class: org.springframework.boot.loader.JarLauncher
複製代碼

org.springframework.boot.loader.JarLauncher 類存放在 org/springframework/boot/loader 下面:

└── boot
    └── loader
        ├── ExecutableArchiveLauncher.class
        ├── JarLauncher.class  # JarLauncher
        ├── # 省略
複製代碼

這樣就基本理清楚了, FatJar 中,org.springframework.boot.loader 下面的類負責引導啓動 SpringBoot 工程,做爲入口,BOOT-INF 中存放業務代碼和依賴,META-INF 下存在元數據描述。

JarLaunch - FatJar 的啓動器

在分析 JarLaunch 以前,這裏插一下,org.springframework.boot.loader 下的這些類是如何被打包在 FatJar 裏面的

spring-boot-maven-plugin 打包 spring-boot-loader 過程

由於在新建的空的 SpringBoot 工程中並無任何地方顯示的引入或者編寫相關的類。實際上,對於每一個新建的 SpringBoot 工程,能夠在其 pom.xml 文件中看到以下插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
複製代碼

這個是 SpringBoot 官方提供的用於打包 FatJar 的插件,org.springframework.boot.loader 下的類其實就是經過這個插件打進去的;

下面是此插件將 loader 相關類打入 FatJar 的一個執行流程:

org.springframework.boot.maven#execute-> org.springframework.boot.maven#repackage -> org.springframework.boot.loader.tools.Repackager#repackage-> org.springframework.boot.loader.tools.Repackager#writeLoaderClasses-> org.springframework.boot.loader.tools.JarWriter#writeLoaderClasses

最終的執行方法就是下面這個方法,經過註釋能夠看出,該方法的做用就是將 spring-boot-loader 的classes 寫入到 FatJar 中。

/** * Write the required spring-boot-loader classes to the JAR. * @throws IOException if the classes cannot be written */
@Override
public void writeLoaderClasses() throws IOException {
	writeLoaderClasses(NESTED_LOADER_JAR);
}
複製代碼

JarLaunch 基本原理

基於前面的分析,這裏考慮一個問題,可否直接經過 java BootStrap 來直接運行 SpringBoot 工程呢?這樣在不須要 -jar 參數和 JarLaunch 引導的狀況下,直接使用最原始的 java 指令理論上是否是也能夠,由於有 main 方法。

經過 java BootStrap 方式啓動

BootStrap 類的以下:

@SpringBootApplication
public class BootStrap {
    public static void main(String[] args) {
        SpringApplication.run(BootStrap.class,args);
    }
}
複製代碼

編譯以後,執行 java com.glmapper.bridge.boot.BootStrap,而後拋出異常了:

Exception in thread "main" java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication
        at com.glmapper.bridge.boot.BootStrap.main(BootStrap.java:13)
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 1 more
複製代碼

從異常堆棧來看,是由於找不到 SpringApplication 這個類;這裏其實仍是比較好理解的,BootStrap 類中引入了 SpringApplication,可是這個類是在 BOOT-INF/lib 下的,而 java 指令在啓動時也沒有指定 class path 。

這裏再也不贅述,經過 -classpath + -Xbootclasspath 的方式嘗試了下,貌似也不行,若是有經過 java 指令直接運行成功的,歡迎留言溝通。

經過 java JarLaunch 啓動

再經過 java org.springframework.boot.loader.JarLauncher 方式啓動,能夠看到是能夠的。

那這裏基本能夠猜到,JarLauncher 方式啓動時,必定會經過某種方式將所須要依賴的 JAR 文件做爲 BootStrap 的依賴引入進來。下面就來簡單分析下 JarLauncher 啓動時,做爲啓動引導類,它作了哪些事情。

基本原理分析

JarLaunch 類的定義以下:

public class JarLauncher extends ExecutableArchiveLauncher {
    // BOOT-INF/classes/
    static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
    // BOOT-INF/lib/
    static final String BOOT_INF_LIB = "BOOT-INF/lib/";
    // 空構造函數
    public JarLauncher() {
    }
    // 帶有指定 Archive 的構造函數
    protected JarLauncher(Archive archive) {
    	super(archive);
    }
    // 是不是可嵌套的對象
    @Override
    protected boolean isNestedArchive(Archive.Entry entry) {
    	if (entry.isDirectory()) {
    		return entry.getName().equals(BOOT_INF_CLASSES);
    	}
    	return entry.getName().startsWith(BOOT_INF_LIB);
    }
    
    // main 函數
    public static void main(String[] args) throws Exception {
    	new JarLauncher().launch(args);
    }

}
複製代碼

經過代碼,咱們很明顯能夠看到幾個關鍵的信息點:

  • BOOT_INF_CLASSESBOOT_INF_LIB 兩個常量對應的是前面解壓以後的兩個文件目錄
  • JarLaunch 中包含一個 main 函數,做爲啓動入口

可是單從 main 來看,只是構造了一個 JarLaunch 對象,而後執行其 launch 方法,並無咱們指望看到的構建所需依賴的地方。實際上這部分是在 JarLaunch 的父類 ExecutableArchiveLauncher 的構造函數中來完成的。

public ExecutableArchiveLauncher() {
    try {
        // 構建 archive 
    	this.archive = createArchive();
    }
    catch (Exception ex) {
    	throw new IllegalStateException(ex);
    }
}

// 構建 Archive
protected final Archive createArchive() throws Exception {
    ProtectionDomain protectionDomain = getClass().getProtectionDomain();
    CodeSource codeSource = protectionDomain.getCodeSource();
    URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
    // 這裏就是拿到當前的 classpath 
    // /Users/xxx/Documents/test/glmapper-springboot-study-guides/guides-for-jarlaunch/target/mock/
    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);
    }
    // 構建 Archive 
    return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
複製代碼

PS: 關於 Archive 的概念這裏因爲篇幅有限,再也不展開說明。

經過上面構建了一個 Archive ,而後繼續執行 launch 方法:

protected void launch(String[] args) throws Exception {
    // 註冊協議,利用了 java.net.URLStreamHandler 的擴展機制,SpringBoot
    // 擴展出了一種能夠解析 jar in jar 的協議
    JarFile.registerUrlProtocolHandler();
    // 經過 classpath 來構建一個 ClassLoader
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    // launch 
    launch(args, getMainClass(), classLoader);
}
複製代碼

下面值須要關注下 getMainClass() 方法便可,這裏就是獲取 MENIFEST.MF 中指定的 Start-Class ,實際上就是咱們的工程裏面的 BootStrap 類:

@Override
protected String getMainClass() throws Exception {
    // 從 archive 中拿到 Manifest
    Manifest manifest = this.archive.getManifest();
    String mainClass = null;
    if (manifest != null) {
        // 獲取 Start-Class
    	mainClass = manifest.getMainAttributes().getValue("Start-Class");
    }
    if (mainClass == null) {
    	throw new IllegalStateException(
    			"No 'Start-Class' manifest entry specified in " + this);
    }
    // 返回 mainClass
    return mainClass;
}
複製代碼

最終是經過構建了一個 MainMethodRunner 實例對象,而後經過反射的方式調用了 BootStrap 類中的 main 方法:

小結

本文主要從 JarLaunch 的角度分析了下 SpringBoot 的啓動方式,對常規 java 方式和 java -jar 等啓動方式進行了簡單的演示;同時簡單闡述了下 JarLaunch 啓動的基本工做原理。對於其中 構建 Archive 、自定義協議 Handler 等未作深刻探究,後面也會針對相關點再作單獨分析。

相關文章
相關標籤/搜索