➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
JasperReports跨程式語言 RSA、AES 加密

Spring Boot 外置 classpath

Table of contents

1 簡介

Spring Boot 有佢自己一套聰明既 classpath resolution 機制,令到 JAR artifact(JAR 檔)既 structure 非常易明,所有 dependency JAR 檔一目了然。
今次我地會探討一下點樣利用呢個機制將 Java classpath 外置喺個 app 既 JAR 檔以外。

2 常見 JAR 檔格式

我地先要了解常見既 JAR 檔格式,包括 Spring Boot。

2.1 Shared library 格式

  • 目的:一個 SDK 或者 utility library,用黎提供 Java APIs,唔可以直接運行。
  • 唔會有 main method(entry point)。
  • 只會有 Java classes、resources 檔。
  • 通常唔會包括 Maven dependency libraries 既 Java classes,但有啲情況下都有可能需要。
  • 會伴隨住一個 POM 檔案,咁用呢個 shared library 既項目就會知道有啲咩需要既 transitive dependencies。
  • JAR 檔係 ZIP 檔格式,而裡面既 root directory 就係 Java classpath。

2.2 Java 標準 runnable app 格式

  • 目的:一個可以直接喺 JVM 上面運行既程式或者系統。
  • 帶有 main method(標準 Java app 既 entry point)。
  • 會有 Java classes、resources 檔。
  • 會係 fat JAR(或者叫 uber JAR),即係會包括所有 Maven dependency libraries 既 Java classes。
  • 可以用 maven-shade-plugin 黎打包成 runnable JAR。
  • JAR 檔係 ZIP 檔格式,而裡面既 root directory 就係 Java classpath。

2.3 Spring Boot runnable app 格式

  • 目的:一個可以直接喺 JVM 上面運行既程式或者系統。
  • 帶有 2main methods。
    • 一個係 Spring Boot Loader 既(標準 Java app 既 entry point)。
    • 一個係 Spring Boot 透過 reflection 執行既。
  • 基於標準 JAR 格式之上,會有一堆額外根據 Spring Boot 自己定義格式既 nested JAR 既檔案。佢同 fat JAR 差唔多,都會包括所有 dependency libraries 既 Java classes。
  • spring-boot-maven-plugin 黎打包成 runnable nested JAR。
  • JAR 檔係 ZIP 檔格式,而裡面既 root directory 就係 Java classpath。不過,當 Spring Boot 啟動之後,就會根據 BOOT-INF/classpath.idx 既訊息,將所有 BOOT-INF/lib 裡面每個 JAR 檔裡面既 root directory 註冊落 custom class loader 作為額外既 Java classpaths。

2.4 Maven 檔案

一般都會用到 Maven 作為 package manager:
  • JAR 檔裡面應該會有一個 META-INF/maven/<groupId>/<artifactId>/pom.xml 檔案。
  • 如果作為 Maven dependency 咁使用,咁就要留意喺 repository 伴隨既 POM 檔裡面既 dependencies。
  • Spring Boot nested JAR 因為格式有別於標準 JAR 格式,所以裡面既 Java classes 冇辦法以 shared library 既形式畀其他項目使用。

3 Spring Boot nested JAR 檔案內容

我地打包 Spring Boot app 既 artifact(web 或者 non-web)都應該用 spring-boot-maven-plugin
呢個 plugin 打包 JAR 既時候,採用既係 Spring Boot 既 custom nested JAR 格式。
Spring Boot nested JAR 既內容大致如下:
  • /BOOT-INF
    • /classes
      • /code(自定義既 package path)
        • MainApplication.classMANIFEST.MFStart-Class attribute)
        • etc
      • application.yml
      • bootstrap.yml(如果用左 spring-cloud-starter-bootstrap
      • etc
    • /lib
      • spring-boot-3.3.5.jar
      • spring-boot-actuator-3.3.5.jar
      • etc
    • classpath.idx
    • layers.idx
  • /META-INF
    • /maven
    • /services
    • BOOT.SF
    • MANIFEST.MF
  • /org
    • /springframework
      • /boot
        • /loader
          • /launch
            • JarLauncher.class
            • PropertiesLauncher.class
            • WarLauncher.class
            • etc
          • etc
檔案路徑描述
BOOT-INF/classes裡面係呢個 Spring Boot 項目所有我自己寫既 Java classes。
BOOT-INF/lib裡面係呢個 Spring Boot 項目所用到既 dependencies(shared libraries)既 JAR 檔。
BOOT-INF/classpath.idx呢個係 Spring 定義既檔案,裡面含有 classpath JAR 檔既次序,例如 BOOT-INF/lib/spring-boot-3.3.5.jar
BOOT-INF/layers.idx只有喺打包成 layered JAR 既情況下先會用到。
META-INF/MANIFEST.MF呢個係標準 JAR 格式既 manifest 描述檔,裡面係 key-value pairs。如果係 runnable JAR,就會有 Main-Class attribute。如果係用 spring-boot-maven-plugin 打包,佢會視乎 packaging type 而將 Main-Class 設定為 JarLauncherWarLauncher 或者 PropertiesLauncher 其中一個。另外,佢亦會加入一個 Spring 定義既 Start-Class attribute(例如 Start-Class: code.MainApplication),而呢個通常就係我地帶有 @SpringBootApplication、有 main method 既 class。
org/springframework/boot/loader裡面係既 Spring Boot Loader 既 Java classes,而呢啲檔案喺每個 Spring Boot 項目裡面都係一樣。
BOOT-INF/classpath.idx
- "BOOT-INF/lib/spring-boot-3.3.5.jar" - "BOOT-INF/lib/spring-boot-autoconfigure-3.3.5.jar" - ...
BOOT-INF/layers.idx
1- "dependencies": 2 - "BOOT-INF/lib/" 3- "spring-boot-loader": 4 - "org/" 5- "snapshot-dependencies": 6- "application": 7 - "BOOT-INF/classes/" 8 - "BOOT-INF/classpath.idx" 9 - "BOOT-INF/layers.idx" 10 - "META-INF/"

4 Spring Boot 啟動流程

  1. JVM 啟動。
  2. Java 執行 META-INF/MANIFEST.MFMain-Class(Spring Boot nested JAR 檔內置既其中一個 Launcher implementation)。
  3. Spring Boot Launcher 會根據 classpath.idx 既 entries 去得到一堆 classpath URLs。
  4. Spring Boot Launcher 會用呢啲 classpath URLs 去創建 LaunchedClassLoader(基於 Java 內置既 URLClassLoader 既 custom class loader)。
  5. Spring Boot Launcher 會將 main thread 既 context class loader 改成用呢個 LaunchedClassLoader
  6. Spring Boot Launcher 會透過 reflection 執行 META-INF/MANIFEST.MFStart-Classmain method。
  7. 呢個 main method 會執行 SpringApplication.run(Class<?>, String...)
註:
  • 任何之後新創建既 threads 既 context class loader 默認都會用佢地 parent thread 既 context class loader,咁就會用左 Spring Boot 創建既 LaunchedClassLoader
  • 喺 Java,class loaders 會根據 delegation model,先 delegate 畀 parent class loader 去 load class,如果失敗,咁 child class loader 先會 find class。

5 實現 Spring Boot 外置 classpath

5.1 Spring Boot Maven Plugin 配置

我地要指定 spring-boot-maven-plugin 去用 ZIPlayout,咁佢先會將 META-INF/MANIFEST.MFMain-ClassJarLauncher 改成 PropertiesLauncher
1<build> 2 <plugins> 3 <plugin> 4 <groupId>org.springframework.boot</groupId> 5 <artifactId>spring-boot-maven-plugin</artifactId> 6 7 <configuration> 8 <layout>ZIP</layout> 9 </configuration> 10 </plugin> 11 </plugins> 12</build>
之後用 mvn clean package 打包成 Spring Boot runnable app 既 nested JAR 檔(app.jar)。

5.2 準備所有外置 classpath 檔案

理論上任何類型既 classpath 檔案都可以由內置變成外置,包括:
  • 一般出現喺 Maven 項目 src/main/resources(對應 Java classpath)既檔案,例如:application.yml
  • 標準 JAR 格式既 shared library JAR 檔,例如 my-lib-1.0.0.jar
我地可以將呢啲檔案都放入一個 folder 裡面(ext folder)。

5.3 啟動 Spring Boot runnable app

我地可以用以下其中一種方式啟動(Windows Command Prompt):
java -Dloader.path=ext -jar app.jar
SET JAVA_TOOL_OPTIONS=-Dloader.path=ext java -jar app.jar
SET loader.path=ext java -jar app.jar
SET LOADER_PATH=ext java -jar app.jar
註:
  • 關於 loader.path
    • Value 以 CSV 格式,可以指定多個 paths,可以用 , 串連起黎,例如 ext/jar,ext/config,my-lib-1.0.0.jar
    • 啲 paths 可以係目錄或者檔案。
    • 啲 paths 可以係 absolute path 或者 relative path。
  • Optional 既 loader.home
    • 畀我地提供 loader.path 所有 paths 既 base directory。
    • 例如我地有 ext/jar 以及 ext/config,咁我地可以用:loader.home=extloader.path=jar,config
  • 就算外置所有 BOOT-INF/lib 既 JAR 檔,Spring Boot 都可以成功啟動。

6 參考資料