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 上面運行既程式或者系統。
- 帶有
2
個 main
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.class
(MANIFEST.MF
既 Start-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 設定為 JarLauncher 、WarLauncher 或者 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 啟動流程
- JVM 啟動。
- Java 執行
META-INF/MANIFEST.MF
既 Main-Class
(Spring Boot nested JAR 檔內置既其中一個 Launcher
implementation)。
- Spring Boot
Launcher
會根據 classpath.idx
既 entries 去得到一堆 classpath URLs。
- Spring Boot
Launcher
會用呢啲 classpath URLs 去創建 LaunchedClassLoader
(基於 Java 內置既 URLClassLoader
既 custom class loader)。
- Spring Boot
Launcher
會將 main
thread 既 context class loader 改成用呢個 LaunchedClassLoader
。
- Spring Boot
Launcher
會透過 reflection 執行 META-INF/MANIFEST.MF
既 Start-Class
既 main
method。
- 呢個
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
去用 ZIP
既 layout
,咁佢先會將 META-INF/MANIFEST.MF
既 Main-Class
由 JarLauncher
改成 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=ext
、loader.path=jar,config
。
- 就算外置所有
BOOT-INF/lib
既 JAR 檔,Spring Boot 都可以成功啟動。
6 參考資料