【SpringBoot】服務 Jar 包的啟動過程原理

酷酷-發表於2024-05-25

1 前言

到現在我碰到的微服務,大多都是打的 Jar包,然後打映象,推映象,釋出。當然也有 War 包的,但是還是比較少。我們這節主要看看 Jar包。 不知道大家有沒有看過 SpringBoot 打好的 Jar 包的內容,以及它是如何啟動的,這節我們就來看看。

2 Jar 包啟動

2.1 單Java Jar 包啟動

有關 java jar包的一些官方說明,我們都知道 Java 能直接執行 Jar 包的,java -jar xxx.jar,那麼我們先看看一個最簡單的 Jar 包。

(1)我這裡有一個簡單的類 JarTest如下:

public class JarTest {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

編譯後的 class:

然後我們新建個清單檔案:META-INF/MANIFEST.MF,內容如下:

Main-Class: JarTest

然後把兩者放到一個壓縮包裡,如下:

然後我們直接執行 java -jar test.jar 可以看到我們的程式執行了。

這個我們的啟動類是在最外層,當我把它放到一個目錄下里,再啟動,也是可以的,這個是我又測試的一個:

那我們再試一個 jar 裡邊套 jar 的,因為我們的 SpringBoot 打包後是不是有很多的第三方依賴的 jar包,所以我們這裡試試這樣的場景再:

看結果貌似是不行,也就是沒有被載入。那我們猜猜是不是要自定義一個類載入,載入像這種第三方依賴的 jar 的呢?那我們下邊看看 SpringBoot 的啟動。

另外,你可能會遇到這個報錯:

其中的一個原因可能就是:你的manifest.mf檔案的格式是否正確。確保每行都以換行符結尾,每個屬性都以“屬性名: 屬性值”格式表示,並且每個屬性之間用換行符分隔。我的就是因為有幾次嘗試的時候,沒有換行一直報清單的錯誤......

2.2 SpringBoot Jar 包啟動

2.2.1 SpringBoot Jar 結構

先看下我們服務平時打好的 Jar 包的內容:

大概分三塊:
(1)BOOT-INF:classes 存放的是我們服務本身的程式碼編譯後的 .class檔案以及 resources 下的一些資源配置檔案,lib存放的是我們服務以來的一些第三方 jar 包(依賴的越多 jar 包越大)

(2)META-INF:這個是 jar 包必備的,java 本身要求的

(3)org:這個裡邊存放的是一些 SpringBoot 啟動需要的類 比如類載入器

至於為什麼需要類載入器?像剛才上邊的我們的例子,我們的 main 類就找不到。對於 Java 標準的 jar 檔案來說,規定在一個 jar 檔案中,我們必須要將指定 main.class 的類直接放置在檔案的頂層目錄中(也就是說,它不予許被巢狀),否則將無法載入,因此 Spring 要想啟動載入,就需要自定義實現自己的類載入器去載入。

2.2.2 SpringBoot Jar 啟動過程

那我們從哪裡開始看起呢?是不是就從清單檔案 MANIFEST.MF 的 Main-Class 開始看起,這個是 Jar 包的啟動入口。接下來我們就從 JarLauncher 看起,先看看它的類圖,其實 Java 就是類,哪哪都是類,你知道的類越多你越牛逼,你在知道類的基礎上還能理清楚它的上下關係也就是類關係那你更牛逼,你能在理清楚上下關係的基礎上用到自己的程式碼裡學以致用加總結其實就 prefect 了,是不是呢?哈哈哈,繼續看我們的 JarLauncher :

看這個圖,是不是我們設計模式裡典型的模板模式,我們這裡主要看 Jar 的哈:

// JarLauncher
public class JarLauncher extends ExecutableArchiveLauncher {
    ...
    // main 方法  
    public static void main(String[] args) throws Exception {
        new JarLauncher().launch(args);
    }
}

作為啟動類,main 方法裡很簡單就一句,例項化然後呼叫 launch 方法。

在例項化的時候,首先會執行父類的例項化 ExecutableArchiveLauncher ,就會開始構建我們的文件類 Archive ,也就是我們的 jar裡邊都有哪些內容,都會封裝進 Archive :

// ExecutableArchiveLauncher 看表面類名  可執行的文件啟動器
public abstract class ExecutableArchiveLauncher extends Launcher {
    // 重要的屬性文件  比我們的這裡的 jar 啟動 我們 jar 包的所有內容是不是都會包裝到 Archive 類裡
    private final Archive archive;

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

// Launcher 類的 createArchive
// createArchive 方法就是根據當前的啟動類的位置,來尋找和封裝 Archive 我們的這裡的 jar 就會是 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));
}

那看到這裡,我們知道的是 JarLauncher 例項化的時候會把我們的 jar 裡的資訊都包進了 Archive 這個類裡,那我們這裡簡單瞭解下這個類。

2.2.2.1 Archive

Archive 即歸檔檔案,這個概念在linux下比較常見,通常就是一個tar/zip格式的壓縮包,jar 就是 zip 格式,SpringBoot抽象了Archive的概念,一個Archive可以是jar(JarFileArchive),可以是一個檔案目錄(ExplodedArchive),可以抽象為統一訪問資源的邏輯層,關於Spring Boot中Archive的原始碼如下:

public interface Archive extends Iterable<Archive.Entry> {
    // 獲取該歸檔的url
    URL getUrl() throws MalformedURLException;
    // 獲取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF
    Manifest getManifest() throws IOException;
    // 獲取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar
    List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}

該介面有兩個實現,分別是

  • org.springframework.boot.loader.archive.ExplodedArchive
  • org.springframework.boot.loader.archive.JarFileArchive

前者用於在資料夾目錄下尋找資源,後者用於在jar包環境下尋找資源。而在SpringBoot打包的 jar 中,則是使用後者JarFileArchive。

大家也可以開啟 SpringBoot 的原始碼,就有一個專門的 JarLauncherTest 大家可以寫一個測試方法,來用 JarFileArchive 開啟一個平時我們的 jar包看看效果,這是我的:

可以看到對 jar包 的封裝,每個JarFileArchive都會對應一個JarFile。JarFile被構造的時候會解析內部結構,去獲取jar包裡的各個檔案或資料夾,這些檔案或資料夾會被封裝到Entry中,也儲存在JarFileArchive中。如果Entry是個jar,會解析成JarFileArchive。

比如一個JarFileArchive對應的URL為:

jar:file:/D:/JetBrains/yanjiu/spring-boot-2.1.8.RELEASE/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/demo-0.0.1-SNAPSHOT.jar!/

它對應的JarFile為:

D:\JetBrains\yanjiu\spring-boot-2.1.8.RELEASE\spring-boot-project\spring-boot-tools\spring-boot-loader\src\test\resources\demo-0.0.1-SNAPSHOT.jar

這個JarFile有很多Entry,就是我們 Jar包 裡的所有內容:

JarFileArchive內部的一些依賴jar對應的URL(SpringBoot使用org.springframework.boot.loader.jar.Handler處理器來處理這些URL),並且如果有jar包中包含jar,或者jar包中包含jar包裡面的class檔案,那麼會使用 !/ 分隔開,這種方式只有org.springframework.boot.loader.jar.Handler能處理,它是SpringBoot內部擴充套件出來的一種URL協議。

構造JarFileArchive物件,獲取其中所有的資源目標,取得其所有的Url,那我們繼續回到啟動過程:

// Launcher 類的 launch 方法
protected void launch(String[] args) throws Exception {
    JarFile.registerUrlProtocolHandler();
    // 建立類載入器 因為jar in jar的java 是不會載入的,所以這裡建立自己的類載入器進行載入
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    // 獲取 start-class 執行其 main方法 也就是我們服務的 SpringApplication的main方法 
    launch(args, getMainClass(), classLoader);
}
// 建立類載入器
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    List<URL> urls = new ArrayList<>(archives.size());
    for (Archive archive : archives) {
        urls.add(archive.getUrl());
    }
    return createClassLoader(urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
    return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
// 啟動
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
        // 將建立的類載入器放到執行緒上下文中
    Thread.currentThread().setContextClassLoader(classLoader);
    createMainMethodRunner(mainClass, args, classLoader).run();
}
// ExecutableArchiveLauncher 根據 jar 裡的清單檔案找 start-class
@Override
protected String getMainClass() throws Exception {
    Manifest manifest = this.archive.getManifest();
    String mainClass = null;
    if (manifest != null) {
        mainClass = manifest.getMainAttributes().getValue("Start-Class");
    }
    if (mainClass == null) {
        throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
    }
    return mainClass;
}
// 封裝 MainMethodRunner 物件執行 run方法
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
    return new MainMethodRunner(mainClass, args);
}
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) {
        this.mainClassName = mainClass;
        this.args = (args != null) ? args.clone() : null;
    }

    public void run() throws Exception {
                // 從執行緒上下文中的類載入器中載入我們的服務啟動類
        Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
                // 反射獲取到 main 方法進行執行
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        mainMethod.invoke(null, new Object[] { this.args });
    }

}

在Launcher的launch方法中,透過以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目錄所對應的archive,透過這些archives的url生成LaunchedURLClassLoader,並將其設定為執行緒上下文類載入器,啟動應用。

至此,才執行我們應用程式主入口類的main方法,所有應用程式類檔案均可透過/BOOT-INF/classes載入,所有依賴的第三方jar均可透過/BOOT-INF/lib載入。

3 小結

好啦,本節主要看下 SpringBoot 的 jar 大體的啟動過程,我們可能還有一些小細節,比如 JarFileArchive 詳細去收集每個目錄下的檔案的過程(可能是遞迴或者某種迴圈收集)以及 SpringBoot 的類載入器實際載入每個類的過程(類載入器載入過程)沒去細看了哈,我們知道的是它把建立的類載入已經放到上下文中了,可以透過上下文中載入我們的類,本節就暫時瞭解到這裡,有理解不對的地方歡迎指正哈。

相關文章