1. 程式人生 > >Java如何訪問Jar包內部的Jar包資源

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] classjava的傳參, * [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在每個不同的平臺都有不同的實現, 但是總體執行方式是類似的。原始碼參考

Created with Raphaël 2.1.2
開始拆解命令(實現於 java.c)系統類載入將拆解的資訊注入JVM,例如classpath使用者類載入結束

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語言編寫的載入器, 它會負責載入ExtClassLoaderAppClassLoader在內的一系列java.*sun.*的類檔案。

而包含ExtClassLoaderAppClassLoader在內的類載入器, 實質性的類載入也需要依託於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/libBOOT-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實現類載入。 且看下述流程圖:

Created with Raphaël 2.1.2JarLauncher.main建立類載入器 LaunchedURLClassLoader載入位置:Jar包根目錄下BOOT/class;BOOT/lib class拆解方案:去掉 BOOT/class 字首直接得到 類lib拆解方案:提供JarFile(重寫了java.util.jar.JarFile)lib類類檔案拆解方案:提供JarEntry(重寫了 java.util.jar.JarEntry)使用者class.main

其中Spring Boot Maven外掛會重寫 JarFileJarEntry 等一系列相關的類。 其中的根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>