Java如何訪問Jar包內部的Jar包資源
引言
日常擼程式碼,肯定會遇見這樣的情況:
- 寫了一個普通Java程式碼,需要引用很多的第三方Jar包,但是隻能把主程式碼和第三方程式碼分別存放,然後打包
- Spring-Boot就可以把第三方Jar包打到自己的可執行包裡面去
- 無奈只能把所有的Jar包拆散,畢並不知道怎麼把他們打在一起
- 很多時候指定的
-classpath
這個命令列命令不生效
針對如上的問題, 本文將系統的進行一次解答。
Java 命令
Java 啟動程序的命令的組成
如下資訊皆摘錄並縮減於 Java doc
java啟動程序有兩種命令, 命令組成如下:
- java [ options ] class [ arguments ]
- java [ options ] -jar file.jar [ arguments ]
他們分別對應著類檔案和Jar檔案的啟動方式。
程序啟動命令與其它的命令無差別,都是可執行程式 + 傳參, 其中, [options] class 是java
的傳參, * [arguments] *是class
的傳參。
Java的Options有如下六類:
- Standard Options 所有Java規範裡面都支援的傳參,如
-D/-version/-jar
等 - Non-Standard Options Oracle官方(Java HotSpot Virtual Machine)支援的規範,如
-Xdebug/-Xmn/-Xms
- Advanced Runtime Options:對JVM的執行約束,如
-XX:MaxDirectMemorySize/-XX:+PerfDataSaveToFile
等 - Advanced JIT Compiler Options:對 JIT的執行約束
- Advanced Serviceability Options:對系統資訊的相關的引數,如日誌路徑等
- Advanced Garbage Collection Options:與垃圾回收相關的約束
Java 命令的執行過程
Java
在每個不同的平臺都有不同的實現, 但是總體執行方式是類似的。原始碼參考
Java 傳參中的 classpath 不生效
日常編碼中都會引用一些第三方的Jar包, 通常是使用
-classpath
命令將其加入到JVM中的, 可是有的時候會出現對於Jar包使用-classpath
命令的時候不生效的情況。 這又是為什麼呢?
這個需要參考原始碼片段:
// ··· 其它程式碼
while ((arg = *argv) != 0 && *arg == '-') {
argv++; --argc;
if (JLI_StrCmp(arg, "-classpath") == 0 || JLI_StrCmp(arg, "-cp") == 0) {
ARG_CHECK (argc, ARG_ERROR1, arg);
SetClassPath(*argv);
mode = LM_CLASS;
argv++; --argc;
} else if (JLI_StrCmp(arg, "-jar") == 0) {
ARG_CHECK (argc, ARG_ERROR2, arg);
mode = LM_JAR;
}
// ··· 其它程式碼
}
// ··· 其它程式碼
Java命令會通過-classpath
命令確定當前的可執行檔案為類檔案, 或者通過-jar
命令確定當前可執行檔案為Jar包。如果兩者都沒有被發現, 則預設為類檔案
對於Jar包,如果同時出現-classpath
命令, 會發生什麼事情呢? 答案是 -classpath
命令會被忽略。
參考:-jar引數執行應用時classpath的設定方法
為什麼不生效? 我想是因為既定的Jar包, Java設計的時候,classpath就需要參考到Jar裡面的manifest吧。
不生效的原因?目前還沒有翻到這相關的原始碼。希望有知道的人介紹一下。
對於 -jar 引數中的 -classpath,有這麼幾種解決方案
- java 官方提供方案:
-Xbootclasspath:
/-Xbootclasspath/a:
/-Xbootclasspath/p
,其中只有第二個被Java推薦 - 將jar包放置在
{Java_home}\jre\lib\ext
下, 會自動被Extension ClassLoader
載入 - 使用
Class-Path
Manifest擴充套件(主要應對於maven構建) - 不要在Manifest中包含主函式,而是依舊使用
-classpath
命令, 可執行選項變更為類檔案
(推薦)
Java 的Jar包 的類載入
Java的類載入有一個很顯著的特點,便是必須顯式的指定ClassLoader的載入區域
- BootstrapClassLoader負責載入
${JAVA_HOME}/jre/lib
部分jar包 - ExtClassLoader載入
${JAVA_HOME}/jre/lib/ext
下面的jar包 - AppClassLoader載入使用者自定義
-classpath
或者Jar包的Class-Path
定義的第三方包
其中BootstrapClassLoader
為C語言編寫的載入器, 它會負責載入ExtClassLoader
和AppClassLoader
在內的一系列java.*
和sun.*
的類檔案。
而包含ExtClassLoader
和AppClassLoader
在內的類載入器, 實質性的類載入也需要依託於Java的 JNI 機制, 原始碼參見 OpenJDK的hotspot/src/share/vm
。
如何使得Java程式讀取Jar包內部的Jar包
目前直觀意義上的Java是沒法讀取Jar包內部的Jar包, 如下圖
run.jar
|——org
| |——springframework
| |——boot
| |——loader
| |——JarLauncher.class
| |——Launcher.class
|——META-INF
| |——MANIFEST.MF
|——BOOT-INF
| |——class
| |——Main.class
| |——Begin.class
| |——lib
| |——commons.jar
| |——plugin.jar
| |——resource
| |——a.jpg
| |——b.jpg
對於Java而言, classpath
可搜尋區域只有org一層。在BOOT-INF/lib
和BOOT-INF/class
裡面的檔案不屬於classloader搜素物件(如果編寫了相對路徑依然可以訪問到內部的資源),直接訪問的話會報NoClassDefDoundErr
異常。
Java
本身支援訪問Jar包裡面的資源, 他們以 Stream
的形式存在(他們本就處於Jar包之中)。Jar檔案被描述為JarFile
, 裡面的資原始檔被描述為JarEntry
。可以通過判斷JarEntry
的Jar屬性使得直接訪問Jar包內部的Jar包。
如果希望直接在自己的類裡面訪問引用在 Jar包中的Jar包, 可以使用Spring Boot打包外掛。強烈推薦該方案, 對所有的Jar專案實用
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
Spring Boot的Jar包內類載入原理
通過訪問Spring Boot打包之後的類檔案可以知道, Manifest 中的啟動類是Spring Boot的自定義程式碼org.springframework.boot.loader.JarLauncher
。 它所處的位置位於Jar包的根目錄,剛好被Java類載入機制所支援。
而位於其中的 BOOT/class
目錄往往是不可用的,畢竟一般人編碼包名不會叫做 BOOT.*
其載入原理則是通過自定義類載入器LaunchedURLClassLoader
實現類載入。 且看下述流程圖:
其中Spring Boot Maven外掛
會重寫 JarFile
和 JarEntry
等一系列相關的類。 其中的根Jar包為打包之後的Jar包, 修飾方案依然是Java原生的JarFile, 其內部的一級JarEntry同為原生。
但是其二級Jar包(根Jar包裡面的Jar包)的修飾方案則為Spring Boot Maven外掛
提供的實現方案,使用 !/
來連結一級和二級Jar包, 實現檔案的訪問及類載入。
因此, 使用Spring Boot Maven外掛
啟動的Java程式碼的類載入器都是LaunchedURLClassLoader
, 載入路徑則都在根Jar包之下。通過將根Jar包裡面的每個一級檔案的URL新增到自己的Classpath下令自己可以直接訪問, 通過將每個一級檔案Jar包構造成JarFile使得他的類載入機制可以延續Java本身的類載入方案。
Java啟動命令推薦方案
如何直接訪問位於Jar包中的Jar包。以及訪問其中的class檔案和類檔案。
不處於 Maven 倉庫環境下
這樣的環境編碼往往只能自行新增Jar包進行服務。 此時最佳的方案不是自己構建Manifest, 而是將所有依賴的Jar包打成一個zip包, 需要用的時候解壓, 然後使用 “-cp 或者 -classpath
“命令將所有的Jar包、資源包括在一起,後跟Main方法。
處於 Maven 倉庫環境下
直接使用Spring Boot Maven外掛
, 省去一切煩惱。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>