1. 程式人生 > >Java 9 揭祕(8. JDK 9重大改變)

Java 9 揭祕(8. JDK 9重大改變)

Tips
做一個終身學習的人。

Java 9

在本章,主要介紹以下內容:

  • 新的JDK版本控制方案是什麼
  • 如何使用Runtime.Version類解析JDK版本字串
  • JDK JRE 9的新目錄佈局是什麼
  • JDK 9中的批註的標準覆蓋機制如何工作的
  • 在JDK 9中使用擴充套件機制的變化
  • JDK 9中的類載入器如何工作以及模組的載入方式
  • 資源如何封裝在JDK 9中的模組中
  • 如何使用ModuleClassClassLoader類中的資源查詢方法訪問模組中的資源
  • jrt URL方案是什麼,以及如何使用它來訪問執行時映像中的資源
  • 如何訪問JDK 9中的JDK內部API以及JDK 9中已刪除的JDK API列表
  • JDK 9中如何使用--patch-module命令列選項替換模組中的類和資源

一. 新的JDK版本控制方案

在JDK 9之前,JDK版本控制方案對開發人員來說並不直觀,程式解析並不容易。 看看這兩個JDK版本,你不能說出他們之間的微妙差異。 很難回答一個簡單的問題:哪個版本包含最新的安全修復程式,JDK 7 Update 55或JDK 7 Update 60? 答案不是很明顯的,你可能已經猜到了JDK 7 Update 60。這兩個版本都包含相同的安全修復程式。 JDK 8 Update 66,1.8.0_66和JDK 8u66版本有什麼區別? 它們代表相同的版本。 在瞭解版本字串中包含的詳細資訊之前,有必要詳細瞭解版本控制方案。 JDK 9試圖規範JDK版本控制方案,因此人們很容易理解,易於程式解析,並遵循行業標準版本控制方案。

JDK 9包含一個名為Runtime.Version的靜態巢狀類,它表示Java SE平臺實現的版本字串。 它可以用於表示,解析,驗證和比較版本字串。

版本字串按順序由以下四個元素組成。 只有第一個是強制性的:

  • 版本號
  • 預發行資訊
  • 構建資訊
  • 附加資訊

以下正則表示式定義版本字串的格式:

$vnum(-$pre)?(\+($build)?(-$opt)?)?

一個簡短版本的字串由一個版本號碼組成,可選地包含預釋出資訊:

$vnum(-$pre)?

可以使用只包含主版本號“9”的版本字串。“9.0.1-ea + 154-20170130.07.36am”,包含版本字串的所有部分。

1. 版本號

版本號是按句點分隔的元素序列。 它可以是任意長度。 其格式如下:

^[1-9][0-9]*(((\.0)*\.[1-9][0-9]*)*)*$

版本號可以由一到四個元素組成,如下所示:

$major.$minor.$security(.$addtionalInfo)

$major元素代表JDK版本的主要版本。 主要版本是遞增的,其中包含重要的新功能。 例如,JDK 8的主要版本為8,對於JDK 9為9。當主版本號增加時,版本號中的所有其他部分都將被刪除。 例如,如果版本號為9.2.2.1,則主版本號從9增加到10時,新版本號將為10。

$minor元素代表JDK版本的次要版本。 增加一個小的更新版本,例如錯誤修復,新的垃圾收集器,新的JDK特定的API等。

$security元素表示JDK版本的安全級別更新。 它會增加一個安全更新。 當次要版本號增加時,該元素不會重置。 給定$major$security更高值總是表示更安全的版本。 例如,JDK版本9.1.7與JDK版本9.5.7一樣安全,因為兩個版本的安全級別是相同的,也就是7。另一個例子,JDK版本9.2.2比9.2.1更安全,因為對於相同的主要版本9,前者的安全級別為2大於後者的安全級別1。

以下規則適用於版本號:

  • 所有元素必須是非負整數。
  • 前三個要素分別被視為主要版本,次要版本和安全級別;其餘的(如果存在)被視為附加資訊,例如指示補丁釋出的數字。
  • 只有主要版本元素是強制性的。
  • 版本號的元素不能包含前導零。 例如,JDK 9的主要版本是9,而不是09。
  • 後面的元素不能為零。 也就是說,版本號不能為9.0.0。 它可以是9,9.2或9.0.x,其中x是正整數。

2. 預發行資訊

版本字串中的$pre元素是預發行識別符號,例如早期訪問版本的ea,預發行版快照,以及開發人員內部構建版本。 這是可選的。 如果它存在,它以字首為連字元( - ),並且必須是與正則表示式([a-zA-Z0-9] +)匹配的字母數字字串)。 以下版本字串包含9作為版本號,ea作為預釋出資訊。

9-ea

3. 構建資訊

版本字串中的$build元素是為每個提升的構建增加的構建號。 這是可選的。當版本號的任何部分增加時,它將重置為1。 如果它存在,它加上加號(+),並且必須匹配正則表示式(0 | [1-9] [0-9] *)。 以下版本的字串包含154作為版本號。

9-EA+154

4. 附加資訊

版本字串中的$opt元素包含其他構建資訊,例如內部構建的日期和時間。這是可選的。它是字母和數字,可以包含連字元和句點。 如果它存在,它以字首為連字元(-),並且必須與正則表示式([-a-zA-Z0-9 。] +)匹配。 如果$build不存在,則需要在$opt值前加一個加號,後跟連字元(+ -)來指定$opt的值。 例如,在9-ea+132-2016-08-23中,$build為132,$opt為2016-08-23; 在9+-123中,$pre$build缺失,$opt為123。以下版本字串在其附加資訊元素中加入釋出的日期和時間:

9-EA+154-20170130.07.36am

5. 解析舊版和新版字串

JDK版本或者是受限更新版本,其中包括新功能和非安全修補程式,或重要補丁更新,其中僅包括針對安全漏洞的修補程式。 版本字串包括版本號,包括更新號和構建號。 限制更新版本的編號為20的倍數。重要補丁更新使用奇數,通過將五加倍加到先前的限制更新中,並在需要時新增一個以保持計算結果為奇數。 一個例子是1.8.0_31-b13,它是JDK主版本8的更新31。 它的內部版本號是13。注意,在JDK 9之前,版本字串始終以1開頭。

Tips
解析版本字串以獲取JDK版本的主版本的現有程式碼可能會在JDK 9中失敗,具體取決於其使用的邏輯。 例如,如果邏輯通過跳過第一個元素(以前為1)來查詢第二個元素的主版本,邏輯將失敗。 例如,如果它從1.8.0返回8,那麼它將從9.0.1返回0,在那裡你會期望9。

6. 系統屬性的版本更改

在JDK 9中,包含JDK版本字串的系統屬性返回的值已更改。 下面表格是這些系統屬性及其格式的列表。 $vstr$vnum$pre分別指版本字串,版本號和預釋出資訊。

系統屬性名稱
java.version \(vnum(\-\)pre)?
java.runtime.version $vstr
java.vm.version $vstr
java.specification.version $vnum
java.vm.specification.version $vnum

7. 使用Runtime.Version

DK 9添加了一個名為Runtime.Version的靜態巢狀類,其例項代表版本字串。 Version類沒有公共建構函式。 獲取其例項的唯一方法是呼叫靜態方法parse(String vstr)。 如果版本字串為空或無效,該方法可能會丟擲執行時異常。

import java.lang.Runtime.Version;
...
// Parse a version string "9.0.1-ea+132"
Version version =  Version.parse("9.0.1-ea+132");

Runtime.Version類中的以下方法返回版本字串的元素。 方法名稱足夠直觀,可以猜測它們返回的元素值的型別。

int major()
int minor()
int security()
Optional<String> pre()
Optional<Integer> build()
Optional<String> optional()

注意,對於可選元素,$pre$build$opt,返回型別為Optional。 對於可選的$minor$security元素,返回型別為int,而不是Optional,如果版本字串中缺少$minor$security,則返回零。

回想一下,版本字串中的版本號可能包含第三個元素之後的附加資訊。 Version類不包含直接獲取附加資訊的方法。 它包含一個version()方法,該方法返回List<Integer>,其中列表包含版本號的所有元素。 列表中的前三個元素是$major$minor$security。 其餘元素包含附加版本號資訊。

Runtime.Version類包含在次序和等式方面比較兩個版本字串的方法。 可以比較它們或者不包含可選的構建資訊($opt)。 這些比較方法如下:

int compareTo(Version v)
int compareToIgnoreOptional(Version v)
boolean equals(Object v)
boolean equalsIgnoreOptional(Object v)

如果v1小於等於或大於v2,表示式v1.compareTo(v2)將返回負整數,零或正整數。 compareToIgnoreOptional()方法的工作方式與compareTo()方法相同,只不過它在比較時忽略了可選的構建資訊。 equals()equalsIgnoreOptional()方法將兩個版本字串進行比較,不包含可選構建資訊。

哪個版本的字串代表最新版本:9.1.1或9.1.1-ea? 第一個不包含預發行元素,而第二個字串包含,所以第一個是最新版本。 哪個版本的字串代表最新版本:9.1.1或9.1.1.1-ea? 這一次,第二個代表最新的版本。 比較發生在序列$vnum$pre$build$opt。 當版本號較大時,不比較版本字串中的其他元素。

此部分的原始碼位於名為com.jdojo.version.string的模組中,其宣告如下所示。

// module-info.java
module com.jdojo.version.string {
    exports com.jdojo.version.string;
}

下面程式碼包含一個完整的程式,顯示如何使用Runtime.Version類來提取版本字串的所有部分。

com.jdojo.version.string
// VersionTest.java
package com.jdojo.version.string;
import java.util.List;
import java.lang.Runtime.Version;
public class VersionTest {
    public static void main(String[] args) {
        String[] versionStrings = {
            "9", "9.1", "9.1.2", "9.1.2.3.4", "9.0.0",
            "9.1.2-ea+153", "9+132", "9-ea+132-2016-08-23", "9+-123",
            "9.0.1-ea+132-2016-08-22.10.56.45am"};
        for (String versonString : versionStrings) {
            try {
                Version version = Version.parse(versonString);
                // Get the additional version number elements
                // which start at 4th element
                String vnumAdditionalInfo = getAdditionalVersionInfo(version);
                System.out.printf("Version String=%s%n", versonString);
                System.out.printf("Major=%d, Minor=%d, Security=%d, Additional Version=%s,"
                        + " Pre=%s, Build=%s, Optional=%s %n%n",
                        version.major(),
                        version.minor(),
                        version.security(),
                        vnumAdditionalInfo,
                        version.pre().orElse(""),
                        version.build().isPresent() ? version.build().get().toString() : "",
                        version.optional().orElse(""));
            } catch (Exception e) {
                System.out.printf("%s%n%n", e.getMessage());
            }
        }
    }
    // Returns the version number elements from the 4th elements to the end
    public static String getAdditionalVersionInfo(Version v) {
        String str = "";
        List<Integer> vnum = v.version();
        int size = vnum.size();
        if (size >= 4) {
            str = str + String.valueOf(vnum.get(3));
        }
        for (int i = 4; i < size; i++) {
            str = str + "." + String.valueOf(vnum.get(i));
        }
        return str;
    }
}

VersionTest類,顯示如何使用Runtime.Version類來處理版本字串。
下面是輸出結果:

Version String=9
Major=9, Minor=0, Security=0, Additional Version=, Pre=, Build=, Optional=
Version String=9.1
Major=9, Minor=1, Security=0, Additional Version=, Pre=, Build=, Optional=
Version String=9.1.2
Major=9, Minor=1, Security=2, Additional Version=, Pre=, Build=, Optional=
Version String=9.1.2.3.4
Major=9, Minor=1, Security=2, Additional Version=3.4, Pre=, Build=, Optional=
Invalid version string: '9.0.0'
Version String=9.1.2-ea+153
Major=9, Minor=1, Security=2, Additional Version=, Pre=ea, Build=153, Optional=
Version String=9+132
Major=9, Minor=0, Security=0, Additional Version=, Pre=, Build=132, Optional=
Version String=9-ea+132-2016-08-23
Major=9, Minor=0, Security=0, Additional Version=, Pre=ea, Build=132, Optional=2016-08-23
Version String=9+-123
Major=9, Minor=0, Security=0, Additional Version=, Pre=, Build=, Optional=123
Version String=9.0.1-ea+132-2016-08-22.10.56.45am
Major=9, Minor=0, Security=1, Additional Version=, Pre=ea, Build=132, Optional=2016-08-22.10.56.45am

二. JDK和JRE的改變

JDK和JRE已經在Java SE 9中進行了模組化處理。對結構進行了一些修改。 還進行了一些其他更改,以提高效能,安全性和可維護性。 大多數這些變化會影響類庫開發人員和IDE開發人員,而不是應用程式開發人員。為了討論這些變化,把它們分為三大類:

  • 佈局變化
  • 行為變化
  • API更改

以下部分將詳細介紹這些改變。

1. JDK和JRE的佈局變化

結構更改會影響執行時映像中的目錄和檔案的組織方式,並影響其內容。 在Java SE 9之前,JDK構建系統用於生成兩種型別的執行時映像 ——Java執行時環境(JRE)和Java開發工具包(JDK)。 JRE是Java SE平臺的完整實現,JDK包含了JRE和開發工具和類庫。 可下圖顯示了Java SE 9之前的JDK安裝中的主目錄。JDK_HOME是安裝JDK的目錄。 如果你只安裝了JRE,那麼你只有在jre目錄下的目錄。

Java SE 9之前的JDK和JRE目錄佈局

在 Java SE 9之前,JDK中:

  • bin目錄用於包含命令列開發和除錯工具,如javac,jar和javadoc。 它還用於包含Java命令來啟動Java應用程式。
  • include目錄包含在編譯原生代碼時使用的C/C++標頭檔案。
  • lib目錄包含JDK工具的幾個JAR和其他型別的檔案。 它有一個tools.jar檔案,其中包含javac編譯器的Java類。
  • jre\bin目錄包含基本命令,如java命令。 在Windows平臺上,它包含系統的執行時動態連結庫(DLL)。
  • jre\lib目錄包含使用者可編輯的配置檔案,如.properties和.policy檔案。
  • jre\lib\approved目錄包含允許使用標準覆蓋機制的JAR。 這允許在Java社群程序之外建立的實施標準或獨立技術的類和介面的更高版本被併入Java平臺。 這些JAR被新增到JVM的引導類路徑中,從而覆蓋了Java執行時中存在的這些類和介面的任何定義。
  • jre\lib\ext目錄包含允許擴充套件機制的JAR。 該機制通過擴充套件類載入器(該類載入器)載入了該目錄中的所有JAR,該引導類載入器是系統類載入器的子程序,它載入所有應用程式類。 通過將JAR放在此目錄中,可以擴充套件Java SE平臺。 這些JAR的內容對於在此執行時映像上編譯或執行的所有應用程式都可見。
  • jre\lib目錄包含幾個JAR。 rt.jar檔案包含執行時的Java類和資原始檔。 許多工具依賴於rt.jar檔案的位置。
  • jre\lib目錄包含用於非Windows平臺的動態連結本地庫。
  • jre\lib目錄包含幾個其他子目錄,其中包含執行時檔案,如字型和影象。

JDK和JRE的根目錄包含多個檔案,如COPYRIGHT,LICENSE和README.html。 根目錄中的發行檔案包含一個描述執行時映像(如Java版本,作業系統版本和體系結構)的鍵值對。 以下程式碼顯示了JDK 8中的示例版本檔案的部分內容:

JAVA_VERSION="1.8.0_66"
OS_NAME="Windows"
OS_VERSION="5.2"
OS_ARCH="amd64"
BUILD_TYPE="commercial"

Java SE 9調整了JDK的目錄層次結構,並刪除了JDK和JRE之間的區別。 下圖顯示了Java SE 9中JDK安裝的目錄。JDK 9中的JRE安裝不包含include和jmods目錄。

Java SE 9中的JDK目錄佈局

在Java SE 9 的JDK中:

  • 沒有名為jre的子目錄。
  • bin目錄包含所有命令。 在Windows平臺上,它繼續包含系統的執行時動態連結庫。
  • conf目錄包含使用者可編輯的配置檔案,例如以前位於jre\lib目錄中的.properties和.policy檔案。
  • include目錄包含要在以前編譯原生代碼時使用的C/C++標頭檔案。 它只存在於JDK中。
  • jmods目錄包含JMOD格式的平臺模組。 建立自定義執行時映像時需要它。 它只存在於JDK中。
  • legal 目錄包含法律宣告。
  • lib目錄包含非Windows平臺上的動態連結本地庫。 其子目錄和檔案不應由開發人員直接編輯或使用。

JDK 9的根目錄有如COPYRIGHT和README等檔案。 JDK 9中的發行檔案包含一個帶有MODULES鍵的新條目,其值為映像中包含的模組列表。 JDK 9映像中的發行檔案的部分內容如下所示:

MODULES=java.rmi,jdk.jdi,jdk.policytool
OS_VERSION="5.2"
OS_ARCH="amd64"
OS_NAME="Windows"
JAVA_VERSION="9"
JAVA_FULL_VERSION="9-ea+133"

在列表中只顯示了三個模組。 在完整的JDK安裝中,此列表將包括所有平臺模組。 在自定義執行時映像中,此列表將僅包含你在映像中使用的模組。

Tips
JDK中的lib\tools.jar和JRE中的lib\rt.jar已從Java SE 9中刪除。這些JAR中可用的類和資源現在以檔案中的內部格式儲存在lib目錄的命名模組中。 可以使用稱為jrt的新方案來從執行時映像檢索這些類和資源。 依靠這些JAR位置的應用程式將不再工作。

2. 行為變化

行為變化將影響應用程式的執行時行為。 以下部分將說明這些更改。

三. 支援標準覆蓋機制

在Java SE 9之前,可以使用支援標準的覆蓋機制來使用更新版本的類和介面來實現支援標準或獨立API,如javax.rmi.CORBA包和Java API for XML Processing(JAXP) ,它們是在Java社群程序之外建立的。 這些JAR已經被新增到JVM的引導類路徑中,從而覆蓋了JRE中存在的這些類和介面的任何定義。 這些JAR的位置由名為java.endorsed.dirs的系統屬性指定,其中目錄由特定於平臺的路徑分隔符字元分隔。 如果未設定此屬性,則執行時將在jre\lib\approved目錄中查詢JAR。

Java SE 9仍然支援認可的標準和獨立API覆蓋機制。 在Java SE 9中,執行時映像由模組組成。 要使用此機制,需要使用更新版本的模組,用於支援標準和獨立API。 需要使用--upgrade-module-path命令列選項。 此選項的值是包含“承認標準”和“獨立API”模組的目錄列表。 Windows上的以下命令將覆蓋“標準標準”模組,如JDK 9中的java.corba模組。將使用umod1和umod2目錄中的模組而不是執行時映像中的相應模組:

java --upgrade-module-path umod1;umod2 <other-options>

Tips
在Java SE 9中,建立一個JAVA_HOME\lib\approvaled目錄並設定名為java.endorsed.dirs的系統屬性,會產生錯誤。

四. 擴充套件機制

版本9之前的Java SE允許擴充套件機制,可以通過將JAR放置在系統屬性java.ext.dirs指定的目錄中來擴充套件執行時映像。 如果未設定此係統屬性,則使用jre\lib\ext目錄作為其預設值。 該機制通過擴充套件類載入器(這是引導類載入器的子類)和系統類載入器的父級載入了該目錄中的所有JAR。 它載入所有應用程式類。 這些JAR的內容對於在此執行時映像上編譯或執行的所有應用程式都可見。

Java SE 9不支援擴充套件機制。 如果需要類似的功能,可以將這些JAR放在類路徑的前面。 使用名為JAVA_HOME\lib\ext的目錄或設定名為java.ext.dirs的系統屬性會導致JDK 9中的錯誤。

1. 類載入器的改變

在程式執行時,每個型別都由類載入器載入,該類由java.lang.ClassLoader類的一個例項表示。 如果你有一個物件引用obj,你可以通過呼叫obj.getClass().getClassLoader()方法獲得它的類載入器引用。 可以使用其getParent()方法獲取類載入器的父類。

在版本9之前,JDK使用三個類載入器來載入類,如下圖所示。 圖中箭頭方向表示委託方向。 可以新增更多的類載入器,這是ClassLoader類的子類。 來自不同位置和型別的JDK載入類中的三個類載入器。

版本9之前的JDK中的類載入器層次結構

JDK類載入器以分層方式工作 —— 引導類載入器位於層次結構的頂部。 類載入器將類載入請求委託給上層類載入器。 例如,如果應用程式類載入器需要載入一個類,它將請求委託給擴充套件類載入器,擴充套件類載入器又將請求委託給引導類載入器。 如果引導類載入器無法載入類,擴充套件類載入器將嘗試載入它。 如果擴充套件類載入器無法載入類,則應用程式類載入器嘗試載入它。 如果應用程式類載入器無法載入它,則丟擲ClassNotFoundException異常。

引導類載入器是擴充套件類載入器的父類。 擴充套件類載入器是應用程式類載入器的父類。 引導類載入器沒有父類。 預設情況下,應用程式類載入器將是你建立的其他類載入器的父類。

引導類載入器載入由Java平臺組成的引導類,包括JAVA_HOME\lib\rt.jar中的類和其他幾個執行時JAR。 它完全在虛擬機器中實現。 可以使用-Xbootclasspath/p-Xbootclasspath/a命令列選項來附加引導目錄。 可以使用-Xbootclasspath選項指定引導類路徑,該選項將替換預設的引導類路徑。 在執行時,sun.boot.class.path系統屬性包含引導類路徑的只讀值。 JDK通過null表示這個類載入器。 也就是說,你不能得到它的引用。 例如,Object類由引導類載入器載入,並且Object.class.getClassLoade()表示式將返回null。

擴充套件類載入器用於通過java.ext.dirs系統屬性指定的目錄中的位於JAR中的擴充套件機制載入可用的類。要獲得擴充套件類載入器的引用,需要獲取應用程式類載入器的引用,並在該引用上使用getParent()方法。

應用程式類載入器從由CLASSPATH環境變數指定的應用程式類路徑或命令列選項-cp-classpath載入類。應用程式類載入器也稱為系統類載入器,這是一種誤稱,它暗示它載入系統類。可以使用ClassLoader類的靜態方法getSystemClassLoader()獲取對應用程式類載入器的引用。

JDK 9保持三級分層類載入器架構以實現向後相容。但是,從模組系統載入類的方式有一些變化。 JDK 9類載入器層次結構如下圖所示。

JDK 9中的載入器層次結構

請注意,在JDK 9中,應用程式類載入器可以委託給平臺類載入器以及引導類載入器;平臺類載入器可以委託給引導類載入器和應用程式類載入器。 以下詳細介紹JDK 9類載入器的工作原理。

在JDK 9中,引導類載入器是由類庫和程式碼在虛擬機器中實現的。 為了向後相容,它在程式中仍然由null表示。 例如,Object.class.getClassLoader()仍然返回null。 但是,並不是所有的Java SE平臺和JDK模組都由引導類載入器載入。 舉幾個例子,引導類載入器載入的模組是java.basejava.loggingjava.prefsjava.desktop。 其他Java SE平臺和JDK模組由平臺類載入器和應用程式類載入器載入,這在下面介紹。 JDK 9中不再支援用於指定引導類路徑,-Xbootclasspath-Xbootclasspath/p選項以及系統屬性sun.boot.class.path-Xbootclasspath/a選項仍然受支援,其值儲存在jdk.boot.class.path.append的系統屬性中。

JDK 9不再支援擴充套件機制。 但是,它將擴充套件類載入器保留在名為平臺類載入器的新名稱下。 ClassLoader類包含一個名為getPlatformClassLoader()的靜態方法,該方法返回對平臺類載入器的引用。 下表包含平臺類載入器載入的模組列表。 平臺類載入器用於另一目的。 預設情況下,由引導類載入器載入的類將被授予所有許可權。 但是,幾個類不需要所有許可權。 這些類在JDK 9中已經被取消了特權,並且它們被平臺類載入器載入以提高安全性。

下面是JDK 9中由平臺載入器載入的模組列表。

java.activation
java.xml.ws.annotation
jdk.desktop
java.compiler
javafx.base
jdk.dynalink
java.corba
javafx.controls
jdk.javaws
java.jnlp
javafx.deploy
jdk.jsobject
java.scripting
javafx.fxml
jdk.localedata
java.se
javafx.graphics
jdk.naming.dns
java.se.ee
javafx.media
jdk.plugin
java.security.jgss
javafx.swing
jdk.plugin.dom
java.smartcardio
javafx.web
jdk.plugin.server
java.sql
jdk.accessibility
jdk.scripting.nashorn
java.sql.rowset
jdk.charsets
jdk.security.auth
java.transaction
jdk.crypto.cryptoki
jdk.security.jgss
java.xml.bind
jdk.crypto.ec
jdk.xml.dom
java.xml.crypto
jdk.crypto.mscapi
jdk.zipfs
java.xml.ws
jdk.deploy

應用程式類載入器載入在模組路徑上找到的應用程式模組和一些提供工具或匯出工具API的JDK模組,如下表所示。 仍然可以使用ClassLoader類的getSystemClassLoader()的靜態方法來獲取應用程式類載入器的引用。

jdk.attach
jdk.jartool
jdk.jstatd
jdk.compiler
jdk.javadoc
jdk.pack
jdk.deploy.controlpanel
jdk.jcmd
jdk.packager
jdk.editpad
jdk.jconsole
jdk.packager.services
jdk.hotspot.agent
jdk.jdeps
jdk.policytool
jdk.internal.ed
jdk.jdi
jdk.rmic
jdk.internal.jvmstat
jdk.jdwp.agent
jdk.scripting.nashorn.shell
jdk.internal.le
jdk.jlink
jdk.xml.bind
jdk.internal.opt
jdk.jshell
jdk.xml.ws

Tips
在JDK 9之前,擴充套件類載入器和應用程式類載入器是java.net.URLClassLoader類的一個例項。 在JDK 9中,平臺類載入器(以前的擴充套件類載入器)和應用程式類載入器是內部JDK類的例項。 如果你的程式碼依賴於·URLClassLoader·類的特定方法,程式碼可能會在JDK 9中崩潰。

JDK 9中的類載入機制有所改變。 三個內建的類載入器一起協作來載入類。 當應用程式類載入器需要載入類時,它將搜尋定義到所有類載入器的模組。 如果有合適的模組定義在這些類載入器中,則該類載入器將載入類,這意味著應用程式類載入器現在可以委託給引導類載入器和平臺類載入器。 如果在為這些類載入器定義的命名模組中找不到類,則應用程式類載入器將委託給其父類,即平臺類載入器。 如果類尚未載入,則應用程式類載入器將搜尋類路徑。 如果它在類路徑中找到類,它將作為其未命名模組的成員載入該類。 如果在類路徑中找不到類,則丟擲ClassNotFoundException異常。

當平臺類載入器需要載入類時,它將搜尋定義到所有類載入器的模組。 如果一個合適的模組被定義為這些類載入器中,則該類載入器載入該類。 這意味著平臺類載入器可以委託給引導類載入器以及應用程式類載入器。 如果在為這些類載入器定義的命名模組中找不到一個類,那麼平臺類載入器將委託給它的父類,即引導類載入器。

當引導類載入器需要載入一個類時,它會搜尋自己的命名模組列表。 如果找不到類,它將通過命令列選項-Xbootclasspath/a指定的檔案和目錄列表進行搜尋。 如果它在引導類路徑上找到一個類,它將作為其未命名模組的成員載入該類。

你可以看到類載入器及其載入的模組和類。 JDK 9包含一個名為-Xlog::modules的選項,用於在虛擬機器載入時記錄除錯或跟蹤訊息。 其格式如下:

-Xlog:modules=<debug|trace>

此選項產生大量的輸出。 建議將輸出重定向到一個檔案,以便可以輕鬆檢視。 以下命令在Windows上執行素數檢查的客戶端程式,並在test.txt檔案中記錄模組載入資訊。 下面顯示部分輸出。 輸出顯示定義模組的類載入器。
命令:

C:\Java9Revealed>java -Xlog:modules=trace --module-path lib
 --module com.jdojo.prime.client/com.jdojo.prime.client.Main > test.txt

部分資訊輸出:

[0.022s][trace][modules] Setting package: class: java.lang.Object, package: java/lang, loader: <bootloader>, module: java.base
[0.022s][trace][modules] Setting package: class: java.io.Serializable, package: java/io, loader: <bootloader>, module: java.base
...
[0.855s][debug][modules] define_module(): creation of module: com.jdojo.prime.client, version: NULL, location: file:///C:/Java9Revealed/lib/com.jdojo.prime.client.jar, class loader 0x00000049ec86dd90 a 'jdk/internal/loader/ClassLoaders$AppClassLoader'{0x00000000895d1c98}, package #: 1
[0.855s][trace][modules] define_module(): creation of package com/jdojo/prime/client for module com.jdojo.prime.client
...

五. 訪問資源

資源是應用程式使用的資料,例如影象,音訊,視訊,文字檔案等。Java提供了一種通過在類路徑上定位資源來訪問資源的位置無關的方式。 需要以與在JAR中打包類檔案相同的方式打包資源,並將JAR新增到類路徑。 通常,類檔案和資源打包在同一個JAR中。 訪問資源是每個Java開發人員執行的重要任務。 在接下來的章節中,將在版本9和JDK 9之前解釋JDK中提供可用的API。

1. 在JDK 9之前訪問資源

在本節中,將解釋如何在版本9之前在JDK中訪問資源。如果你已經知道如何在版本9之前訪問JDK中的資源,可以跳到下一節,介紹如何訪問JDK 9中的資源。

在Java程式碼中,資源由資源名稱標識,資源名稱是由斜線(/)分隔的一串字串。 對於儲存在JAR中的資源,資源名稱僅僅是儲存在JAR中的檔案的路徑。 例如,在JDK 9之前,儲存在rt.jar中的java.lang包中的Object.class檔案是一個資源,其資源名稱是java/lang/Object.class。

在JDK 9之前,可以使用以下兩個類中的方法來訪問資源:

java.lang.Class
java.lang.ClassLoader

資源由ClassLoader定位。 一個Class代理中的資源尋找方法到它的ClassLoader。 因此,一旦瞭解ClassLoader使用的資源載入過程,將不會在使用Class類的方法時遇到問題。 在兩個類中有兩種不同的命名例項方法:

URL getResource(String name)
InputStream getResourceAsStream(String name)

兩種方法都會以相同的方式找到資源。 它們的差異僅在於返回型別。 第一個方法返回一個URL,而第二個方法返回一個InputStream。 第二種方法相當於呼叫第一種方法,然後在返回的URL物件上呼叫openStream()

Tips
如果找不到指定的資源,所有資源查詢方法都將返回null。

ClassLoader類包含三個額外的查詢資源的靜態方法:

static URL getSystemResource(String name)
static InputStream getSystemResourceAsStream(String name)
static Enumeration<URL> getSystemResources(String name)

這些方法使用系統類載入器(也稱為應用程式類載入器)來查詢資源。 第一種方法返回找到的第一個資源的URL。 第二種方法返回找到的第一個資源的InputStream。 第三種方法返回使用指定的資源名稱找到的所有資源的URL列舉。

要找到資源,有兩種型別的方法可以從——getSystemResource *getResource *中進行選擇。 在討論哪種方法是最好的之前,重要的是要了解有兩種型別的資源:

  • 系統資源
  • 非系統資源

你必須瞭解他們之間的區別,以瞭解資源查詢機制。系統資源是在bootstrap類路徑,擴充套件目錄中的JAR和應用程式類路徑中找到的資源。非系統資源可以儲存在除路徑之外的位置,例如在特定目錄,網路上或資料庫中。 getSystemResource()方法使用應用程式類載入程式找到一個資源,委託給它的父類,它是擴充套件類載入器,後者又委託給它的父類(引導類載入器)。如果你的應用程式是獨立的應用程式,並且它只使用三個內建的JDK類載入器,那麼你將很好的使用名為getSystemResource *的靜態方法。它將在類路徑中找到所有資源,包括執行時映像中的資源,如rt.jar檔案。如果你的應用程式是在瀏覽器中執行的小程式,或在應用程式伺服器和Web伺服器中執行的企業應用程式,則應使用名為getResource*的例項方法,它可以使用特定的類載入器來查詢資源。如果在Class物件上呼叫getResource*方法,則會使用當前類載入器(載入Class物件的類載入器)來查詢資源。

傳遞給ClassLoader類中所有方法的資源名稱都是絕對的,它們不以斜線(/)開頭。 例如,當呼叫ClassLoadergetSystemResource()方法時,將使用java/lang/Object.class作為資源名稱。

Class類中的資源查詢方法可以指定絕對和相對資源名稱。 絕對資源名稱以斜線開頭,而相對資源名稱不用。 當使用絕對名稱時,Class類中的方法會刪除前導斜線並委派給載入Class物件的類載入器來查詢資源。 以下呼叫

Test.class.getResource("/resources/test.config");
會被轉換成
Test.class.getClassLoader().getResource("resources/test.config");

當使用相對名稱時,Class類中的方法預先添加了包名稱,在使用斜線後跟斜線替換包名中的點,然後再委託載入Class物件的類載入器來查詢資源。 假設測試類在com.jdojo.test包中,以下呼叫:
Test.class.getResource("resources/test.config");
會被轉換成
Test.class.getClassLoader() .getResource("com/jdojo/test/resources/test.config");

我們來看一個在JDK 9之前查詢資源的例子。 使用JDK 8執行示例。NetBeans專案名為com.jdojo.resource.preJDK9。 如果你建立自己的專案,請確保將專案的Java平臺和源更改為JDK 8。類和資源的排列如下:
word_to_number.properties
com/jdojo/resource/prejdk9/ResourceTest.class
com/jdojo/resource/prejdk9/resources/number_to_word.properties

該專案包含兩個資原始檔:根目錄下的word_to_number.properties和com/jdojo/resource/prejdk9/resources目錄中的number_to_word.properties。 這兩個屬性檔案的內容分別如下所示:

One=1
Two=2
Three=3
Four=4
Five=5
1=One
2=Two
3=Three
4=Four
5=Five

下面包含一個完整的程式,顯示如何使用不同的類及其方法查詢資源。 該程式演示了可以將應用程式中的類檔案用作資源,可以使用相同的方法找到它們來查詢其他型別的資源。

// ResourceTest.java
package com.jdojo.resource.prejdk9;
import java.io.IOException;
import java.net.URL;
import java.util.Properties;
public class ResourceTest {
    public static void main(String[] args) {
        System.out.println("Finding resources using the system class loader:");
        findSystemResource("java/lang/Object.class");
        findSystemResource("com/jdojo/resource/prejdk9/ResourceTest.class");
        findSystemResource("com/jdojo/prime/PrimeChecker.class");
        findSystemResource("sun/print/resources/duplex.png");
        System.out.println("\nFinding resources using the Class class:");
        // A relative resource name - Will not find Object.class
        findClassResource("java/lang/Object.class");
        // An absolute resource name - Will find Object.class
        findClassResource("/java/lang/Object.class");
        // A relative resource name - will find the class
        findClassResource("ResourceTest.class");
        // Load the wordtonumber.properties file
        loadProperties("/wordtonumber.properties");
        // Will not find the properties because we are using
        // an absolute resource name
        loadProperties("/resources/numbertoword.properties");
        // Will find the properties
        loadProperties("resources/numbertoword.properties");
    }
    public static void findSystemResource(String resource) {
        URL url = ClassLoader.getSystemResource(resource);
        System.out.println(url);
    }
    public static URL findClassResource(String resource) {
        URL url = ResourceTest.class.getResource(resource);
        System.out.println(url);
        return url;
    }
    public static Properties loadProperties(String resource) {
        Properties p1 = new Properties();
        URL url = ResourceTest.class.getResource(resource);
        if (url == null) {
            System.out.println("Properties not found: " + resource);
            return p1;
        }
        try {
            p1.load(url.openStream());
            System.out.println("Loaded properties from " + resource);
            System.out.println(p1);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
        return p1;
    }
}

以下是輸出結果:

Finding resources using the system class loader:
jar:file:/C:/java8/jre/lib/rt.jar!/java/lang/Object.class
file:/C:/Java9Revealed/com.jdojo.resource.prejdk9/build/classes/com/jdojo/resource/prejdk9/ResourceTest.class
null
jar:file:/C:/java8/jre/lib/resources.jar!/sun/print/resources/duplex.png
Finding resources using the Class class:
null
jar:file:/C:/java8/jre/lib/rt.jar!/java/lang/Object.class
file:/C:/Java9Revealed/com.jdojo.resource.prejdk9/build/classes/com/jdojo/resource/prejdk9/ResourceTest.class
Loaded properties from /wordtonumber.properties
{One=1, Three=3, Four=4, Five=5, Two=2}
Properties not found: /resources/numbertoword.properties
Loaded properties from resources/numbertoword.properties
{5=Five, 4=Four, 3=Three, 2=Two, 1=One}

2. 在JDK 9 中訪問資源

在JDK 9之前,可以從類路徑上的任何JAR訪問資源。 在JDK 9中,類和資源封裝在模組中。 在第一次嘗試中,JDK 9設計人員強制執行模組封裝規則,模組中的資源必須對該模組是私有的,因此它們只能在該模組內的程式碼中訪問。 雖然這個規則在理論上看起來很好,但是對於跨模組共享資源的框架和載入的類檔案作為來自其他模組的資源,就會帶來問題。 為了有限地訪問模組中的資源,做了一些妥協,但是仍然強制執行模組的封裝。 JDK 9包含三類資源查詢方法:

java.lang.Class
java.lang.ClassLoader
java.lang.Module

ClassClassLoader類沒新增任何新的方法。 Module類包含一個getResourceAsStream(String name)方法,如果找到該資源,返回一個InputStream;否則返回null。

六. 資源命名語法

資源使用由斜線分隔的字串序列命名,例如com/jdojo/states.png,/com/jdojo/words.png和logo.png。 如果資源名稱以斜線開頭,則被視為絕對資源名稱。

使用以下規則從資源名稱中估算包(package)的名稱:

  • 如果資源名稱以斜線開頭,刪除第一個斜線。 例如,對於資源名稱/com/jdojo/words.png,此步驟將導致com/jdojo/words.png。
  • 從最後一個斜線開始刪除資源名稱中的所有字元。 在這個例子中,com/jdojo/words.png導致com/jdojo。
  • 用點號(.)替換名稱中的每個剩餘的斜線。 所以,com/jdojo被轉換成com.jdojo。 生成的字串是包名稱。

有些情況下使用這些步驟會導致一個未命名的包或一個無效的包名稱。 包名稱(如果存在)必須由有效的Java識別符號組成。 如果沒有包名稱,它被稱為未命名的包。 例如,將META-INF/resource /logo.png視為資源名稱。 應用上一組規則,其包名稱將被計算為“META-INF.resources”,它不是有效的包名,但它是資源的有效路徑。

七. 查詢資源的規則

由於向後相容性和對模組系統的強封裝的承諾,JDK 9中查詢資源的新規則是複雜的,基於以下幾個因素:

  • 包含資源的模組型別:命名的,開放的,未命名的或自動命名的模組;
  • 正在訪問資源的模組:它是同一個模組還是另一個模組?
  • 正在被訪問的資源的包名稱:它是否是有效Java包? 這是一個未命名的包?
  • 封裝包含資源的包:將包含資源的包匯出,開啟或封裝到訪問資源的模組?
  • 正在訪問的資源的副檔名:資源是.class檔案還是其他型別的檔案?
  • 正在使用哪種類的方法來訪問資源:ClassClassLoaderModule類?

以下規則適用於包含資源的命名模組:

  • 如果資源名稱以.class結尾,則可以通過任何模組中的程式碼訪問資源。 也就是說,任何模組都可以訪問任何命名模組中的類檔案。
  • 如果從資源名稱計算的包名稱不是有效的Java包名稱,例如META-INF.resources,則可以通過任何模組中的程式碼訪問該資源。
  • 如果從資源名稱計算的包名稱是未命名的包,例如對於資源名稱(如word.png),則可以通過任何模組中的程式碼訪問該資源。
  • 如果包含該資源的軟體包對訪問該資源的模組開放,則資源可以通過該模組中的程式碼訪問。 一個包對模組開放,因為定義包的模組是一個開放的模組,或者模組開啟所有其他模組的包,或者模組只使用一個限定的開啟語句開啟包。 如果沒有以任何這些方式開啟包,則該包中的資源不能被該模組外的程式碼訪問。
  • 這個規則是上一個規則的分支。 開啟未命名,自動或開放模組中的每個包,因此所有其他模組中的程式碼都可以訪問這些模組中的所有資源。

Tips
命名模組中的包必須開啟,而不是匯出,以訪問其資源。 匯出一個模組的包允許其他模組訪問該包中的公共型別(而不是資源)。

在訪問命名模組中的資源時,ModuleClassClassLoader類中的各種資源查詢方法的行為有所不同:

  • 可以使用Module類的getResourceAsStream()方法來訪問模組中的資源。 此方法是呼叫方敏感的。 如果呼叫者模組不同,則此方法將應用所有資源可訪問性規則,如上所述。
  • 在指定模組中定義的類的Class類中的getResource *()方法僅在該命名模組中定位資源。 也就是說,不能使用這些方法來定位定義呼叫這些方法的類的命名模組之外的類。
  • ClassLoader類中的getResource *()方法基於前面描述的規則列表來定位命名模組中的資源。 這些方法不是呼叫者敏感的。 在嘗試查詢資源本身之前,類載入器將資源搜尋委託給其父類。 這些方法有兩個例外:1)它們僅在無條件開啟的包中定位資源。 如果使用限定的開啟語句開啟包,則這些方法將不會在這些包中找到資源。 2)它們搜尋在類載入器中定義的模組。

Class物件將僅在它所屬的模組中找到資源。 它還支援以斜線開頭的絕對資源名稱,以及不以斜線開頭的相對資源名稱。 以下是使用Class物件的幾個示例:

// Will find the resource
URL url1 = Test.class.getResource("Test.class");
// Will not find the resource because the Test and Object classes are in different modules
URL url2 = Test.class.getResource("/java/lang/Object.class");
// Will find the resource because the Object and Class classes are in the same module, java.base
URL url3 = Object.class.getResource("/java/lang/Class.class");
// Will not find the resource because the Object class is in the java.base module whereas
// the Driver class is in the java.sql module
URL url4 = Object.class.getResource("/java/sql/Driver.class");

使用Module類定位資源需要具有該模組的引用。 如果可以訪問該模組中的類,則在該Class物件上使用getModule()方法給出了模組引用。 這是獲取模組引用的最簡單方法。 有時候,你把模組名稱作為字串,而不是該模組中的類的引用。 可以從模組名稱中找到模組引用。 模組被組織成由java.lang包中的ModuleLayer類的例項表示的層。 JVM至少包含一個boot 層。 boot層中的模組對映到內建的類載入器 —— 引導類載入器,平臺類載入器和應用程式類載入器。 可以使用ModuleLayer類的boot()靜態方法獲取boot層的引用:

// Get the boot layer
ModuleLayer bootLayer = ModuleLayer.boot();

一旦獲得boot層的引用,可以使用其findModule(String moduleName)方法獲取模組的引用:

// Find the module named com.jdojo.resource in the boot layer
Optional<Module> m = bootLayer.findModule("com.jdojo.resource");
// If the module was found, find a resource in the module
if(m.isPresent()) {
    Module testModule = m.get();
    String resource = "com/jdojo/resource/opened/opened.properties";
    InputStream input = module.getResourceAsStream(resource);
    if (input != null) {
        System.out.println(resource + " found.");
    } else {
        System.out.println(resource + " not found.”);
    }
} else {
    System.out.println("Module com.jdojo.resource does not exist");
}

八. 訪問命名模組中的資源的示例

在本部分中,將看到資源查詢規則的具體過程。 在com.jdojo.resource的模組中打包資源,其宣告如下所示。

// module-info.java
module com.jdojo.resource {
    exports com.jdojo.exported;
    opens com.jdojo.opened;
}

該模組匯出com.jdojo.exported包,並開啟com.jdojo.opened包。

以下是com.jdojo.resource模組中所有檔案的列表:

  • module-info.class
  • unnamed.properties
  • META-INF\invalid_pkg.properties
  • com\jdojo\encapsulated\encapsulated.properties
  • com\jdojo\encapsulated\EncapsulatedTest.class
  • com\jdojo\exported\AppResource.class
  • com\jdojo\exported\exported.properties
  • com\jdojo\opened\opened.properties
  • com\jdojo\opened\OpenedTest.class

有四個類檔案。 在這個例子中,只有module-info.class檔案很重要。 其他類檔案定義一個沒有任何細節的同名的類。 具有.properties副檔名的所有檔案都是資原始檔,其內容在此示例中不重要。 原始碼包含Java9Revealed\com.jdojo.resource目錄中這些檔案的內容。

unnamed.properties檔案在未命名的包中,因此可以通過任何其他模組中的程式碼來定位。 invalid_pkg.properties檔案位於META-INF目錄中,它不是有效的Java包名稱,因此該檔案也可以通過任何其他模組中的程式碼來定位。 com.jdojo.encapsulated包沒有開啟,所以encapsulated.properties檔案不能通過其他模組中的程式碼來找到。 com.jdojo.exported包未開啟,所以export.properties檔案不能通過其他模組中的程式碼來找到。 com.jdojo.opened包是開啟的,所以opened.properties檔案可以通過其他模組中的程式碼來定位。該模組中的所有類檔案可以通過其他模組中的程式碼來定位。

下面清單包含com.jdojo.resource.test模組的模組宣告。本模組中的程式碼將嘗試訪問com.jdojo.resource模組中的資源以及本模組中的資源。你需要將com.jdojo.resource模組新增到此模組路徑以進行編譯。 在 NetBean IDE中com.jdojo.resource.test專案的屬性對話方塊如下圖所示。它將com.jdojo.resource模組新增到其模組路徑。

Adding module to the module path

// module-info.java
module com.jdojo.resource.test {
    requires com.jdojo.resource;
    exports com.jdojo.resource.test;
}

com.jdojo.resource.test模組中的檔案按如下方式排列:

  • module-info.class
  • com\jdojo\resource\test\own.properties
  • com\jdojo\resource\test\ResourceTest.class

該模組包含名為own.properties的資原始檔,該檔案位於com.jdojo.resource.test包中。 own.properties檔案為空。 下面包含ResourceTest類的程式碼。

// ResourceTest
package com.jdojo.resource.test;
import com.jdojo.exported.AppResource;
import java.io.IOException;
import java.io.InputStream;
public class ResourceTest {
    public static void main(String[] args) {
        // A list of resources
        String[] resources = {
            "java/lang/Object.class",
            "com/jdojo/resource/test/own.properties",
            "com/jdojo/resource/test/ResourceTest.class",
            "unnamed.properties",
            "META-INF/invalid_pkg.properties",
            "com/jdojo/opened/opened.properties",
            "com/jdojo/exported/AppResource.class",
            "com/jdojo/resource/exported.properties",
            "com/jdojo/encapsulated/EncapsulatedTest.class",
            "com/jdojo/encapsulated/encapsulated.properties"
        };
        System.out.println("Using a Module:");
        Module otherModule = AppResource.class.getModule();
        for (String resource : resources) {
            lookupResource(otherModule, resource);
        }
        System.out.println("\nUsing a Class:");
        Class cls = ResourceTest.class;
        for (String resource : resources) {
            // Prepend a / to all resource names to make them absolute names
            lookupResource(cls, "/" + resource);
        }
        System.out.println("\nUsing the System ClassLoader:");
        ClassLoader clSystem = ClassLoader.getSystemClassLoader();
        for (String resource : resources) {
            lookupResource(clSystem, resource);
        }
        System.out.println("\nUsing the Platform ClassLoader:");
        ClassLoader clPlatform = ClassLoader.getPlatformClassLoader();
        for (String resource : resources) {
            lookupResource(clPlatform, resource);
        }
    }
    public static void lookupResource(Module m, String resource) {
        try {
            InputStream in = m.getResourceAsStream(resource);
            print(resource, in);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
    public static void lookupResource(Class cls, String resource) {
        InputStream in = cls.getResourceAsStream(resource);
        print(resource, in);
    }
    public static void lookupResource(ClassLoader cl, String resource) {
        InputStream in = cl.getResourceAsStream(resource);
        print(resource, in);
    }
    private static void print(String resource, InputStream in) {
        if (in != null) {
            System.out.println("Found: " + resource);
        } else {
            System.out.println("Not Found: " + resource);
        }
    }
}

下面是具體的輸出:

Using a Module:
Not Found: java/lang/Object.class
Not Found: com/jdojo/resource/test/own.properties
Not Found: com/jdojo/resource/test/ResourceTest.class
Found: unnamed.properties
Found: META-INF/invalid_pkg.properties
Found: com/jdojo/opened/opened.properties
Found: com/jdojo/exported/AppResource.class
Not Found: com/jdojo/resource/exported.properties
Found: com/jdojo/encapsulated/EncapsulatedTest.class
Not Found: com/jdojo/encapsulated/encapsulated.properties
Using a Class:
Not Found: /java/lang/Object.class
Found: /com/jdojo/resource/test/own.properties
Found: /com/jdojo/resource/test/ResourceTest.class
Not Found: /unnamed.properties
Not Found: /META-INF/invalid_pkg.properties
Not Found: /com/jdojo/opened/opened.properties
Not Found: /com/jdojo/exported/AppResource.class
Not Found: /com/jdojo/resource/exported.properties
Not Found: /com/jdojo/encapsulated/EncapsulatedTest.class
Not Found: /com/jdojo/encapsulated/encapsulated.properties
Using the System ClassLoader:
Found: java/lang/Object.class
Found: com/jdojo/resource/test/own.properties
Found: com/jdojo/resource/test/ResourceTest.class
Found: unnamed.properties
Found: META-INF/invalid_pkg.properties
Found: com/jdojo/opened/opened.properties
Found: com/jdojo/exported/AppResource.class
Not Found: com/jdojo/resource/exported.properties
Found: com/jdojo/encapsulated/EncapsulatedTest.class
Not Found: com/jdojo/encapsulated/encapsulated.properties
Using the Platform ClassLoader:
Found: java/lang/Object.class
Not Found: com/jdojo/resource/test/own.properties
Not Found: com/jdojo/resource/test/ResourceTest.class
Not Found: unnamed.properties
Not Found: META-INF/invalid_pkg.properties
Not Found: com/jdojo/opened/opened.properties
Not Found: com/jdojo/exported/AppResource.class
Not Found: com/jdojo/resource/exported.properties
Not Found: com/jdojo/encapsulated/EncapsulatedTest.class
Not Found: com/jdojo/encapsulated/encapsulated.properties

lookupResource()方法過載。 它們使用三個類來定位資源:ModuleClassClassLoader。 這些方法將資源名稱和資源引用傳遞給print()方法來列印訊息。

main()方法準備了一個資源列表,用來使用不同的資源查詢方法查詢。 它儲存了一個String陣列列表:

// A list of resources
String[] resources = {/* List of resources */};

main()方法嘗試使用com.jdojo.resource模組的引用查詢所有資源。 請注意,AppResource類在com.jdojo.resource模組中,因此AppResource.class.getModule()方法返回com.jdojo.resource模組的引用。

System.out.println("Using a Module:");
Module otherModule = AppResource.class.getModule();
for (String resource : resources) {
    lookupResource(otherModule, resource);
}

該程式碼找到com.jdojo.resource模組中未命名、無效和開啟的包中的所有類檔案和資源。 請注意,沒有找到java/lang/Object.class,因為它在java.base模組中,而不在com.jdojo.resource模組中。 同樣的原因找不到com.jdojo.resource.test模組中的資源。

現在,main()方法使用Resource Test類的Class物件來找到相同的資源,它在com.jojo.resource.test模組中。

Class cls = ResourceTest.class;
for (String resource : resources) {
    // Prepend a / to all resource names to make them absolute names
    lookupResource(cls, "/" + resource);
}

Class物件將僅在com.jdojo.resource.test模組中定位資源,這在輸出中是顯而易見的。 在程式碼中,使用斜線預先填寫資源名稱,因為Class類中的資源查詢方法會把資源當作不以斜線開頭的相對資源名稱來對待,並將該類的包名稱新增到該資源名稱。

最後,main()方法使用應用程式和平臺類載入器來定位同一組資源:

ClassLoader clSystem = ClassLoader.getSystemClassLoader();
for (String resource : resources) {
    lookupResource(clSystem, resource);
}
ClassLoader clPlatform = ClassLoader.getPlatformClassLoader();
for (String resource : resources) {
    lookupResource(clPlatform, resource);
}

類載入器將在類載入器本身或其祖先類載入器已知的所有模組中定位資源。 系統類載入器載入com.jdojo.resource和com.jdojo.resource.test模組,因此它可以根據資源查詢規則強制的限制來查詢這些模組中的資源。 即引導類載入器從java.base模組載入Object類,因此係統類載入器可以找到java/lang/Object.class檔案。

平臺類載入器不載入com.jdojo.resource和com.jdojo.resource.test應用程式模組。 在輸出中很明顯.平臺類載入器只發現一個資源,java/lang/Object.class,由父類引導類載入器進行載入。

九. 訪問執行時映像中的資源

我們來看幾個在執行時映像中訪問資源的例子。 在JDK 9之前,可以使用ClassLoader類的getSystemResource()靜態方法。 以下是在JDK 8中查詢Object.class檔案的程式碼:

import java.net.URL;
...
String resource = "java/lang/Object.class";
URL url = ClassLoader.getSystemResource(resource);
System.out.println(url);
// jar:file:/C:/java8/jre/lib/rt.jar!/java/lang/Object.class

輸出顯示使用jar方案返回的URL指向rt.jar檔案。

JDK 9不再在JAR中儲存執行時映像。 它可能在將來更改成內部格式儲存。 JDK提供了一種使用jrt方案以與格式和位置無關的方式訪問執行時資源的方法。 上面程式碼在JDK 9中通過使用jrt方案返回一個URL,而不是jar方案:

jrt:/java.base/java/lang/Object.class

Tips
如果你的程式碼從執行時映像訪問資源,並期望使用jar方案的URL,則需要在JDK 9中進行更改,因為在JDK 9中將使用jrt格式獲取URL。

使用jrt方案的語法如下:

jrt:/<module-name>/<path>

<module-name>是模組的名稱,<path>是模組中特定類或資原始檔的路徑。 <module-name><path>都是可選的。 jrt:/,指的是儲存在當前執行時映像中的所有類和資原始檔。 jrt:/<module-name>是指儲存在<module-name>模組中的所有類和資原始檔。 jrt:/<module-name>/<path>指的是<module-name>模組中名為<path>的特定類或資原始檔。 以下是使用jrt方案引用類檔案和資原始檔的兩個URL的示例:

jrt:/java.sql/java/sql/Driver.class
jrt:/java.desktop/sun/print/resources/duplex.png

第一個URL為java.sql模組中java.sql.Driver類的類檔案命名。 第二個URL是java.desktop模組中的映像檔案sun/print/resources/duplex.png命名。

Tips
可以使用jrt方案訪問執行時映像中的資源,但是在使用ModuleClassClassLoader類中的資源查詢方式是不可訪問的。

可以使用jrt方案建立一個URL。 以下程式碼片段顯示瞭如何吧一個圖片檔案讀入到Image物件中,以及在執行時映像中把一個類檔案讀入到位元組陣列。

// Load the duplex.png into an Image object
URL imageUrl = new URL("jrt:/java.desktop/sun/print/resources/duplex.png");
Image image = ImageIO.read(imageUrl);
// Use the image object here
System.out.println(image);
// Load the contents of the Object.class file
URL classUrl = new URL("jrt:/java.base/java/lang/Object.class");
InputStream input = classUrl.openStream();
byte[] bytes = input.readAllBytes();
System.out.println("Object.class file size: " + bytes.length);

輸出結果為:

[email protected]: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = [email protected] transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 41 heig