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目錄中這些文件的內容。

Tips
源代碼在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);

輸出結果為:

BufferedImage@3e57cd70: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@67b467e9 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 41 height = 24 #numDataElements 4 dataOff[0] = 3
Object.class file size: 1859

什么時候可以使用其他形式的jrt方案,以便表示運行時映像中的所有文件和模塊中的所有文件? 可以使用jrt方案來引用一個模塊來授予Java策略文件的權限。 Java策略文件中的以下條目將為java.activation模塊中的代碼授予所有權限:

grant codeBase "jrt:/java.activation" {
    permission java.security.AllPermission;
}

許多工具和IDE需要枚舉運行時映像中的所有模塊,軟件包和文件。 JDK 9為了jrt URL方案,附帶一個只讀NIO FileSystem提供者。 可以使用此提供者列出運行時映像中的所有類和資源文件。 有一些工具和IDE將在JDK 8上運行,但將支持JDK 9的代碼開發。這些工具還需要獲取JDK 9運行時映像中的類和資源文件列表。 當你安裝JDK 9時,它在lib目錄中包含一個jrt-fs.jar文件。 可以將此JAR文件添加到在JDK 8上運行的工具的類路徑,並使用jrt FileSystem,如下所示。

jrt文件系統包含由斜線(/)表示的根目錄,其中包含兩個名為包和模塊的子目錄:

/
/packages
/modules

以下代碼片段為jrt URL方案創建了一個NIO FileSystem

// Create a jrt FileSystem
FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
The following snippet of code reads an image file and the contents of the Object.class file:
// Load an image from a module
Path imagePath = fs.getPath("modules/java.desktop", "sun/print/resources/duplex.png");
Image image = ImageIO.read(Files.newInputStream(imagePath));
// Use the image object here
System.out.println(image);
// Read the Object.class file contents
Path objectClassPath = fs.getPath("modules/java.base", "java/lang/Object.class");
byte[] bytes = Files.readAllBytes(objectClassPath);
System.out.println("Object.class file size: " + bytes.length);

輸出結果為:

BufferedImage@5f3a4b84: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@5204062d transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 41 height = 24 #numDataElements 4 dataOff[0] = 3
Object.class file size: 1859

以下代碼片段將打印運行時映像中所有模塊中的所有類和資源文件。 類似地,可以為包創建·Path`類列舉運行時映像中的所有包。

// List all modules in the runtime image
Path modules = fs.getPath("modules");
Files.walk(modules)
     .forEach(System.out::println);

輸出結果為:

/modules
/modules/java.base
/modules/java.base/java
/modules/java.base/java/lang
/modules/java.base/java/lang/Object.class
/modules/java.base/java/lang/AbstractMethodError.class
...

我們來看一個從運行時映像訪問資源的完整程序。 下面包含名為com.jdojo.resource.jrt的模塊的模塊聲明。

// module-info.java
module com.jdojo.resource.jrt {
    requires java.desktop;
}

接下來是JrtFileSystem類的源代碼,它位於com.jdojo.resource.jrt模塊中。

// JrtFileSystem.java
package com.jdojo.resource.jrt;
import java.awt.Image;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.imageio.ImageIO;
public class JrtFileSystem {
    public static void main(String[] args) throws IOException {
        // Create a jrt FileSystem
        FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
        // Load an image from a module
        Path imagePath = fs.getPath("modules/java.desktop", "sun/print/resources/duplex.png");
        Image image = ImageIO.read(Files.newInputStream(imagePath));
        // Use the image object here
        System.out.println(image);
        // Read the Object.class file contents
        Path objectClassPath = fs.getPath("modules/java.base", "java/lang/Object.class");
        byte[] bytes = Files.readAllBytes(objectClassPath);
        System.out.println("Object.class file size: " + bytes.length);
        // List 5 packages in the runtime image
        Path packages = fs.getPath("packages");
        Files.walk(packages)
             .limit(5)
             .forEach(System.out::println);
        // List 5 modules’ entries in the runtime image
        Path modules = fs.getPath("modules");
        Files.walk(modules)
             .limit(5)
             .forEach(System.out::println);
    }
}

輸出結果為:

BufferedImage@5bfbf16f: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@27d415d9 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 41 height = 24 #numDataElements 4 dataOff[0] = 3
Object.class file size: 1859
packages
packages/com
packages/com/java.activation
packages/com/java.base
packages/com/java.corba
modules
modules/java.desktop
modules/java.desktop/sun
modules/java.desktop/sun/print
modules/java.desktop/sun/print/resources

JrtFileSystem類,演示使用jrt URL方案從運行時映像訪問資源。
注意,程序僅打包和模塊目錄中的五個條目。 可以訪問java.desktop模塊中的sun/print/resources/duplex.png。 java.desktop模塊不打開sun.print.resources包。 使用ModuleClassClassLoader類中的任何資源查找方法來定位 sun/print/resources/duplex.png將失敗。

十. 使用JDK內部API

JDK由公共API和內部API組成。 公共API旨在用於開發可移植Java應用程序。 JDK中的java.*javax.*org.*包包含在公共API。 如果應用程序僅使用公共API,則可以在支持Java平台的所有操作系統上運行。 這種應用提供的另一個保證是,如果它在JDK版本N中工作,它將繼續在JDK版本N + 1中工作。

com.sun.*sun.*jdk.*包用於實現JDK本身,它們組成內部API,這不意味着由開發人員使用。 內部API不能保證在所有操作系統上運行。 com.sun.*sun.*等軟件包是Oracle JDK的一部分。 如果使用其他供應商的JDK,這些軟件包將不可用。 非Oracle JDK(如IBM的JDK)將使用其他軟件包名稱來實現其內部API。 下圖顯示了不同類別的JDK API。

基於其預期用途的JDK API類別

在JDK 9模塊化之前,可以使用任何JAR的公共類,即使這些類是JDK內部API。 開發人員和一些廣泛使用的庫已經使用JDK內部API來方便,或者由於這些API提供的功能難以在JDK之外實現。 這些類的示例是BASE64EncoderBASE64Decoder。 開發人員為了方便使用它們,它們可以作為sun.misc包中的JDK內部API使用,即使它們不難開發。 另一個廣泛使用的類是sun.misc包中的Unsafe類。 在JDK之外開發一個類來替代Unsafe類,因為它訪問了JDK內部是很困難的。

僅用於方便使用的內部API在JDK之外不被使用,或者它們所存在的支持的替換已經被分類為非關鍵內部API,並且已經封裝在JDK 9中。示例是Sun.misc包中的BASE64EncoderBASE64Decoder類,JDK 8里,Base64.EncoderBase64.Decoder`類作為公共API的一部分添加到java.util包中。

在JDK之外廣泛使用但難以開發的內部API被歸類為關鍵的內部API。 如果存在替換,它們被封裝在JDK 9中。 封裝在JDK 9中但可以使用命令行選項的關鍵內部API已使用@jdk.Exported注解。 JDK 9不提供以下類的替代,這些類被認為是關鍵的內部API。 它們可以通過jdk.unsupported模塊訪問。

com.sun.nio.file.ExtendedCopyOption
com.sun.nio.file.ExtendedOpenOption
com.sun.nio.file.ExtendedWatchEventModifier
com.sun.nio.file.SensitivityWatchEventModifier
sun.misc.Signal
sun.misc.SignalHandler
sun.misc.Unsafe
sun.reflect.Reflection
sun.reflect.ReflectionFactory

Tips
在JDK 9中,大多數JDK內部API已封裝在模塊中,默認情況下不可訪問。但仍然可以使用--add-read非標准命令行選項訪問它們。

以下類中的addPropertyChangeListener()removePropertyChangeListener()方法已在JDK 8中棄用,並已從JDK 9中刪除:

java.util.logging.LogManager
java.util.jar.Pack200.Packer
java.util.jar.Pack200.Unpacker

可以使用位於JAVA_HOME\bin目錄中的jdeps工具來查找代碼在JDK內部API上的類級依賴關系。 還需要使用--jdk-internals選項,如下所示:

jdeps --jdk-internals --class-path <class-path> <input-path>

這里,<input-path>可以是類文件,目錄或JAR文件的路徑。 該命令分析<input-path><class-path>上的所有類。 以下命令打印jersey-common.jar文件中JDK內部API的用法,假設JAR位於C:\Java9Revealed\extlib目錄中。

C:\Java9Revealed>jdeps --jdk-internals extlib\jersey-common.jar

下面是部分輸出:

jersey-common.jar -> jdk.unsupported
   org.glassfish.jersey.internal.util.collection.ConcurrentHashMapV8 -> sun.misc.Unsafe                                    JDK internal API (jdk.unsupported)
org.glassfish.jersey.internal.util.collection.ConcurrentHashMapV8$TreeBin -> sun.misc.Unsafe                                    JDK internal API (jdk.unsupported)
...

十一. 修補模塊內容

有時候,可能需要用另一個版本替換特定模塊的類文件和資源進行測試和調試。 在JDK 9之前,可以使用-Xbootclasspath/p選項來實現此目的。 此選項已在JDK 9中刪除。在JDK 9中,需要使用--patch-module非標准命令行選項。 此選項可用於javac和java命令。 其語法如下:

--patch-module <module-name>=<path-list>

這里,<module-name>是正在替換其內容的模塊的名稱。 <path-list>是包含新模塊內容的JAR或目錄列表; 列表中的每個元素都由特定於主機的路徑分隔符分隔,該字符是Windows上的分號和類UNIX平台上的冒號。

可以對同一命令多次使用--patch-module選項,因此可以修補多個模塊的內容。 可以修補應用程序模塊,庫模塊和平台模塊。

Tips
當使用--patch-module選項時,無法替換module-info.class文件。 試圖這樣做是默認無視的。

現在,我們將運行一個修補com.jdojo.intro模塊的例子。 使用新的Welcome.class文件替換此模塊中的Welcome.class文件。 回想一下,我們在第3章中創建了Welcome類。新類將打印一個不同的消息。 新的類聲明如下所示。 在源代碼中,此類位於com.jdojo.intro.patch 的NetBeans項目中。

// Welcome.java
package com.jdojo.intro;
public class Welcome {
    public static void main(String[] args) {
        System.out.println("Hello Module System.");
        // Print the module name of the Welcome class
        Class<Welcome> cls = Welcome.class;
        Module mod = cls.getModule();
        String moduleName = mod.getName();
        System.out.format("Module Name: %s%n", moduleName);
    }
}

現在,需要使用以下命令為上面新的Welcome類編譯源代碼:

C:\Java9Revealed>javac -Xmodule:com.jdojo.intro
  --module-path com.jdojo.intro\dist
  -d patches\com.jdojo.intro.patch com.jdojo.intro.patch\src\com\jdojo\intro\Welcome.java

即使刪除前兩個選項:-Xmodule-module-path,此命令也將成功。 但是,當編譯平台類(如java.util.Arrays)時,將需要這些選項。 否則,將收到錯誤。-Xmodule選項指定要編譯的源代碼所屬的模塊名稱。 --module-path選項指定在哪里查找-Xmodule選項中指定的模塊。 這些選項用於定位編譯新類所需的其他類。 在這種情況下,Welcome類不依賴於com.jdojo.intro模塊中的任何其他類。 這就是為什么在這種情況下刪除這些選項不會影響結果。-d選項指定編譯的Welcome.class文件的保存位置。

以下是從com.jdojo.intro模塊運行原始Welcome類的命令:

C:\Java9Revealed>java --module-path com.jdojo.intro\dist
--module com.jdojo.intro/com.jdojo.intro.Welcome

輸出結果為:

Welcome to the Module System.
Module Name: com.jdojo.intro

現在是使用修補版本運行Welcome類的時候了。 這是執行此操作的命令:

C:\Java9Revealed>java --module-path com.jdojo.intro\dist
  --patch-module com.jdojo.intro=patches\com.jdojo.intro.patch
  --module com.jdojo.intro/com.jdojo.intro.Welcome

輸出結果為:

Hello Module System.
Module Name: com.jdojo.intro

當使用--patch-module選項時,在搜索模塊路徑之前,模塊系統會搜索此選項中指定的路徑。 請注意,此選項中指定的路徑包含模塊的內容,但這些路徑不是模塊路徑。

十二. 總結

如果將舊版應用程序遷移到JDK 9,JDK 9進行了一些突破性的更改,這點必須注意。

JDK 9中對JDK的非直觀版本控制方案已經進行了改進。JDK版本字符串由以下四個元素組成:版本號,預發布信息,構建信息和附加信息。 只有第一個是強制性的。 正則表達式$vnum(-$pre)?(\+($build)?(-$opt)?)?定義了版本字符串的格式。 一個簡短版本的字符串只包含前兩個元素:一個版本號,可選的是預發布信息。 可以有一個簡短到“9”的版本字符串,其中只包含主版本號。“99.0.1-ea+154-20170130.07.36am”,這個版本字符串包含了所有元素。

JDK 9添加了一個名為Runtime.Version的靜態嵌套類,其實例表示JDK版本字符串。 該類沒有公共構造函數。 獲取其實例的唯一方法是調用其靜態方法名parse(String vstr)。 如果版本字符串為空或無效,該方法可能會拋出運行時異常。 該類包含幾個方法來獲取版本的不同部分。

JDK 9更改了JDK和JRE安裝的目錄布局。 現在,除了JDK安裝包含開發工具和JRE不包含的JMOD格式的平台模塊的拷貝之外,JDK和JRE安裝之間沒有區別。 可以構建自己的JRE(使用jlink工具),它可以包含JRE中需要的JDK的任何部分。

在Java SE 9之前,可以使用“支持的標准覆蓋機制”來使用實現“承認標准”或“獨立API”的較新版本的類和接口。 這些包括在Java Community Process之外創建的javax.rmi.CORBA包和Java API for XML Processing(JAXP)。 Java SE 9仍然支持這種機制。 在Java SE 9中,需要使用--upgrade-module-path命令行選項。 此選項的值是包含標准標准和獨立API的模塊的目錄列表。

在版本9之前的Java SE允許一個擴展機制,可以通過將JAR放在系統屬性java.ext.dirs指定的目錄中來擴展運行時映像。 如果未設置此系統屬性,則使用jre\lib\ext目錄作為其默認值。 Java SE 9不支持擴展機制。 如果需要類似的功能,可以將這些JAR放在類路徑的前面。

在版本9之前,JDK使用三個類加載器來加載類。 他們是引導類加載器,擴展類加載器和系統(應用程序)類加載器。 它們分層排列 —— 沒有父類的引導類加載器,引導類加載器作為擴展類加載器的父類,並擴展類加載器作為系統類加載器作為的父級。 在嘗試加載類型本身之前,類加載器將類型加載要求委托給其父類(如果有)。 JDK 9保持了三類裝載機的向后兼容性。 JDK 9不支持擴展機制,所以擴展類加載器沒有意義。 JDK 9已經將擴展類加載器重命名為平台類加載器,該引用可以使用ClassLoader類的靜態方法getPlatformClassLoader()獲取。 在JDK 9中,每個類加載器加載不同類型的模塊。

在JDK 9中,默認情況下封裝命名模塊中的資源。只有當資源處於未命名,無效或打開的包中時,命名模塊中的資源才能被另一個模塊中的代碼訪問。名稱以.class(所有類文件)結尾的命名模塊中的所有資源都可以通過其他模塊中的代碼訪問。可以使用jrt方案的URL來訪問運行時映像中的任何資源。

在JDK 9之前,可以使用JDK內部API。 JDK 9中的大多數JDK內部API已被封裝。有些通過jdk.unsupported模塊來提供。可以使用jdeps工具和--jdk-internals選項來查找代碼對JDK內部API的類級依賴性。

有時候,可能需要用另一個版本替換特定模塊的類文件和資源進行測試和調試。在JDK 9之前,可以使用已在JDK 9中刪除的-Xbootclasspath/p選項來實現。在JDK 9中,需要使用--patch-module非標准命令行選項。 javac和java命令可以使用此選項。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM