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

Tips
作一個終身學習的人。html

Java 9

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

  • 新的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版本控制方案,所以人們很容易理解,易於程序解析,並遵循行業標準版本控制方案。git

JDK 9包含一個名爲Runtime.Version的靜態嵌套類,它表示Java SE平臺實現的版本字符串。 它能夠用於表示,解析,驗證和比較版本字符串。github

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

  • 版本號
  • 預發行信息
  • 構建信息
  • 附加信息

如下正則表達式定義版本字符串的格式:正則表達式

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

一個簡短版本的字符串由一個版本號碼組成,可選地包含預發佈信息:sql

$vnum(-$pre)?

可使用只包含主版本號「9」的版本字符串。「9.0.1-ea + 154-20170130.07.36am」,包含版本字符串的全部部分。shell

1. 版本號

版本號是按句點分隔的元素序列。 它能夠是任意長度。 其格式以下:數據庫

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

版本號能夠由一到四個元素組成,以下所示:bootstrap

$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命令可使用此選項。

相關文章
相關標籤/搜索