1. 程式人生 > >Java 9 揭祕(9. 打破模組封裝)

Java 9 揭祕(9. 打破模組封裝)

Tips
做一個終身學習的人。

Java 9

在此章節中,主要介紹以下內容:

  • 什麼是打破模組的封裝
  • 如何使用命令列選項將依賴項(新增需要)新增到模組
  • 如何使用--add-exports命令列選項匯出模組的未匯出包,並使用可執行JAR的MANIFEST.MF檔案
  • 如何使用--add-opens命令列選項並使用可執行JAR的MANIFEST.MF檔案開啟模組的非開放包
  • 如何使用--add-reads命令列選項增加模組的可讀性

一. 什麼是打破模組的封裝

JDK 9的主要目標之一是將型別和資源封裝在模組中,並僅匯出其他模組要訪問其公共型別的軟體包。 有時,可能需要打破模組指定的封裝,以啟用白盒測試或使用不受支援的JDK內部API或類庫。 這可以通過在編譯時和執行時使用非標準命令列選項來實現。 具有這些選項的另一個原因是向後相容性。 並不是所有現有的應用程式將完全遷移到JDK 9並將被模組化。 如果這些應用程式需要使用以前是公開的但已經封裝在JDK 9中的庫提供的JDK API或API,則這些應用程式有一種方法可以繼續工作。 其中一些選項具有可以新增到可執行JAR的MANIFEST.MF檔案中的對應屬性,以避免使用命令列選項。

Tips
使用Module API也可以使用每個命令列選項來打破模組的封裝。

雖然可能聽起來像這些選項與JDK 9之前的操作相同,但是在訪問JDK內部API時沒有任何限制。 如果模組中的軟體包未匯出或開啟,則表示模組的設計人員無意在模組外部使用這些軟體包。 這樣的包可能會被修改或甚至從模組中刪除,無需任何通知。 如果仍然使用這些軟體包通過使用命令列選項匯出或開啟它們,可能會面臨破壞應用程式的風險!

二. 命令列選項

模組宣告中的三個模組語句(statement)允許模組封裝其型別和資源,並讓其他模組使用來自第一個模組的封裝型別和資源。 這些語句是exports, opens, 和requires

。 每個模組語句都有一個命令列選項。 對於exportsopens語句,可以在JAR的manifest檔案中使用相應的屬性。 下表列出了這些語句及其相應的命令列選項和清單屬性。 在以下部分詳細描述這些選項。

Module Statement Command-Line Option Manifest Attribute
exports --add-exports Add-Exports
opens --add-opens Add-Opens
requires --add-reads 無屬性可用

Tips
您可以在相同的命令列中多次使用--add-exports--add-opens

--add-reads命令列選項。

1. --add-exports選項

模組宣告中的exports語句將模組中的包匯出到所有或其他模組,因此這些模組可以使用該包中的公共API。 如果程式包未由模組匯出,則可以使用-add-exports的命令列選項匯出程式包。 其語法如下:

--add-exports <source-module>/<package>=<target-module-list>

這裡,<source-module>是將<package>匯出到<target-module-list>的模組,它是以逗號分隔的目標模組名稱列表。 相當於向<source-module>的宣告新增一個限定的exports語句:

module <source-module> {
    exports <package> to <target-module-list>;
    // More statements go here
}

Tips
如果目標模組列表是特殊值ALL-UNNAMED,對於--add-exports選項,模組的包將匯出到所有未命名的模組。 --add-exports選項可用於javac和java命令。

以下選項將java.base模組中的sun.util.logging包匯出到com.jdojo.test和com.jdojo.prime模組:

--add-exports java.base/sun.util.logging=com.jdojo.test,com.jdojo.prime

以下選項將java.base模組中的sun.util.logging包匯出到所有未命名的模組:

--add-exports java.base/sun.util.logging=ALL-UNNAMED

2. --add-opens選項

模組宣告中的opens語句使模組裡面的包對其他模組開放,因此這些模組可以在執行期使用深層反射訪問該程式包中的所有成員型別。 如果一個模組的包未開啟,可以使用--add-opens命令列選項開啟它。 其語法如下:

--add-opens <source-module>/<package>=<target-module-list>

這裡,<source-module>是開啟<package><target-module-list>的模組,它是以逗號分隔的目標模組名稱列表。 相當於向<source-module>的宣告新增一個限定的opens語句:

module <source-module> {
    opens <package> to <target-module-list>;
    // More statements go here
}

Tips
如果目標模組列表是特殊值ALL-UNNAMED,對於--add-opened選項,模組的軟體包對所有未命名的模組開放。 --add-opened選項可用於java命令。 在編譯時使用javac命令使用此選項會生成警告,但沒有影響。

以下選項將java.base模組中的sun.util.logging包對com.jdojo.test和com.jdojo.prime模組開放:

--add-opens java.base/sun.util.logging=com.jdojo.test,com.jdojo.prime

以下選項將java.base模組中的sun.util.logging包對所有未命名的模組開放:

--add-opens java.base/sun.util.logging=ALL-UNNAMED

3.--add-reads 選項

--add-reads選項不是關於打破封裝。 相反,它是關於增加模組的可讀性。 在測試和除錯過程中,即使第一個模組不依賴於第二個模組,模組有時也需要讀取另一個模組。 模組宣告中的requires語句用於聲明當前模組對另一個模組的依賴關係。 可以使用--add-reads命令列選項將可讀性邊緣從模組新增到另一個模組。 這對於將第一個模組新增requires語句具有相同的效果。 其語法如下:

--add-reads <source-module>=<target-module-list>

<source-module>是其定義被更新以讀取<target-module-list>中指定的模組列表的模組,該目標模組名稱是以逗號分隔的列表。 相當於將目標模組列表中每個模組的源模組新增一個requires語句:

module <source-module> {
    requires <target-module1>;
    requires <target-module2>;
    // More statements go here
}

Tips
如果目標模組列表是特殊值ALL-UNNAMED,則對於--add-reads選項,源模組讀 有未命名的模組。 這是命名模組可以讀取未命名模組的唯一方法。 沒有可以在命名模組宣告中使用的等效模組語句來讀取未命名的模組。 此選項在編譯時和執行時可用。

以下選項為com.jdojo.common模組添加了一個讀取邊界,使其讀取jdk.accessibility模組:

--add-reads com.jdojo.common=jdk.accessibility

4. --permit-illegal-access選項

前面提到的三個命令列選項,用於新增exportsopensreads僅用於向後相容。 但是,當需要“非法”訪問(反射訪問模組中型別不可訪問的成員)到幾個模組時,使用這些選項是乏味的。 對於這種情況,java命令可以使用--permit-illegal-access選項。 顧名思義,它允許通過使用深層反射的任何未命名模組(類路徑中的程式碼)的程式碼非法訪問任何命名模組中的型別的成員。 其語法如下:

java --permit-illegal-access <other-options-and-arguments>

--permit-illegal-access選項不允許將命名模組中的程式碼非法訪問其他命名模組中的型別的成員。 在這種情況下,可以將此選項與--add-exports--add-opens--add-reads選項組合使用。

Tips
--permit-illegal-access選項在JDK 9中可用,並將在JDK 10中刪除。使用此選項會在標準錯誤流上列印警告。 一個警告列印一個訊息,規定此選項將在將來的版本中被刪除。 其他警告報告了授予非法訪問的程式碼的詳細資訊,授予非法訪問的程式碼以及授予訪問許可權的選項。

在下一節中介紹一個使用所有這些選項的示例,這些選項允許打破模組封裝。

三. 一個示例

我們來看一下打破封裝的例子。 我使用一個簡單的例子。 它的目的是展示可用於打破封裝的所有概念和命令列選項。

使用之前建立com.jdojo.intro模組作為第一個模組。 它在com.jdojo.intro包中包含一個Welcome類。 該模組不匯出包,所以Welcome類被封裝,不能在模組外部訪問。 在這個例子中,從另一個模組com.jdojo.intruder呼叫Welcome類的main()方法。其宣告如下所示。

// module-info.java
module com.jdojo.intruder {
    // No module statements
}

下面顯示了此模組中TestNonExported類的程式碼。

// TestNonExported.java
package com.jdojo.intruder;
import com.jdojo.intro.Welcome;
public class TestNonExported {
    public static void main(String[] args) {
        Welcome.main(new String[]{});
    }
}

TestNonExported類只包含一行程式碼。 它呼叫Welcome類的靜態方法main()傳遞一個空的String陣列。 如果該類被編譯並執行,則在執行Welcome類時列印與第3章中相同的訊息:

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

編譯com.jdojo.intruder模組的程式碼:

C:\Java9Revealed>javac --module-path com.jdojo.intro\dist
-d com.jdojo.intruder\build\classes
com.jdojo.intruder\src\module-info.java com.jdojo.intruder\src\com\jdojo\intruder\TestNonExported.java

如果收到如下錯誤:

com.jdojo.intruder\src\com\jdojo\intruder\TestNonExported.java:4: error: package com.jdojo.intro is not visible
import com.jdojo.intro.Welcome;
                ^
  (package com.jdojo.intro is declared in module com.jdojo.intro, but module com.jdojo.intruder does not read it)
1 error

該命令使用--module-path選項將com.jdojo.intro模組包含在模組路徑上。 編譯時錯誤指向匯入com.jdojo.intro.Welcome類的import語句。 它宣告包com.jdojo.intro對於com.jdojo.intruder模組是不可見的。 也就是說,com.jdojo.intro模組不匯出包含Welcome類的com.jdojo.intro包。 要解決此錯誤,需要使用--add-exports命令列選項將com.jdojo.intro模組的com.jdojo.intro包匯出到com.jdojo.intruder模組中:

C:\Java9Revealed>javac --module-path com.jdojo.intro\dist
--add-exports com.jdojo.intro/com.jdojo.intro=com.jdojo.intruder
-d com.jdojo.intruder\build\classes
com.jdojo.intruder\src\module-info.java com.jdojo.intruder\src\com\jdojo\intruder\TestNonExported.java

但是仍然報錯:

warning: [options] module name in --add-exports option not found: com.jdojo.intro
com.jdojo.intruder\src\com\jdojo\intruder\TestNonExported.java:4: error: package com.jdojo.intro is not visible
import com.jdojo.intro.Welcome;
                ^
  (package com.jdojo.intro is declared in module com.jdojo.intro, but module com.jdojo.intruder does not read it)
1 error
1 warning

這一次,你會得到警告和錯誤。 錯誤與以前相同。 該警告訊息指出編譯器找不到com.jdojo.intro模組。 因為這個模組沒有依賴關係,所以即使在模組路徑中也沒有解決這個模組。 要解決警告,需要使用--add-modules選項將com.jdojo.intro模組新增到預設的根模組中:

C:\Java9Revealed>javac --module-path com.jdojo.intro\dist
--add-modules com.jdojo.intro
--add-exports com.jdojo.intro/com.jdojo.intro=com.jdojo.intruder
-d com.jdojo.intruder\build\classes
com.jdojo.intruder\src\module-info.java
com.jdojo.intruder\src\com\jdojo\intruder\TestNonExported.java

即使com.jdojo.intruder模組未讀取com.jdojo.intro模組,此javac命令仍然成功。 這似乎是一個錯誤。 如果它不是一個bug,那麼沒有找到支援這種行為的文件。 稍後,將看到java命令將不適用於相同的模組。 如果此命令出錯,並顯示一條訊息,表示TestNonExported類無法訪問Welcome類,請新增以下選項來修復它:

--add-reads com.jdojo.intruder=com.jdojo.intro

嘗試使用以下命令重新執行TestNonExported類,該命令包括模組路徑上的com.jdojo.intruder模組:

C:\Java9Revealed>java --module-path com.jdojo.intro\dist;com.jdojo.intruder\build\classes
--add-modules com.jdojo.intro
--add-exports com.jdojo.intro/com.jdojo.intro=com.jdojo.intruder
--module com.jdojo.intruder/com.jdojo.intruder.TestNonExported

但是會報出以下錯誤資訊:

Exception in thread "main" java.lang.IllegalAccessError: class com.jdojo.intruder.TestNonExported (in module com.jdojo.intruder) cannot access class com.jdojo.intro.Welcome (in module com.jdojo.intro) because module com.jdojo.intruder does not read module com.jdojo.intro
        at com.jdojo.intruder/com.jdojo.intruder.TestNonExported.main(TestNonExported.java:8)

錯誤資訊已經很清晰。 它宣告com.jdojo.intruder模組必須讀取com.jdojo.intro模組,以便前者使用後者的Welcome類。 可以使用--add-reads選項來修復錯誤,該選項將在com.jdojo.intruder模組中新增一個讀取邊界(等同於requires語句)以讀取com.jdojo.intro模組。 以下命令執行此操作:

C:\Java9Revealed>java --module-path com.jdojo.intro\dist;com.jdojo.intruder\build\classes
--add-modules com.jdojo.intro
--add-exports com.jdojo.intro/com.jdojo.intro=com.jdojo.intruder
--add-reads com.jdojo.intruder=com.jdojo.intro
--module com.jdojo.intruder/com.jdojo.intruder.TestNonExported

輸出結果為:

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

這一次,你會收到所期望的輸出。 下圖顯示了執行此命令時建立的模組圖。

模組圖

com.jdojo.intruder和com.jdojo.intro模組都是根模組。 com.jdojo.intruder模組被新增到預設的根模組中,因為正在執行的主類在此模組中。 com.jdojo.intro模組通過--add-modules選項新增到預設的根模組集中。 通過--add-reads選項從com.jdojo.intruder模組將一個讀取邊界新增到com.jdojo.intro模組。 模組圖中,使用虛線顯示了從前者到後者的讀取,以便在構建模組圖之後作為--add-reads選項的結果新增它。 使用此命令使用-Xdiag:resolver選項來檢視模組的解決方法。

來看看另一個例子,它將展示如何使用--add-opens命令列選項開啟一個包到另一個模組。 在第4章中,有一個com.jdojo.address模組,其中包含com.jdojo.address包中的Address類。 該模組匯出com.jdojo.address包。 該類包含一個名為line1的私有欄位, 有一個public getLine1()方法返回line1欄位的值。

如下程式碼所示,TestNonOpen類嘗試載入Address類,建立類的例項,並訪問其公共和私有成員。 TestNonOpen類是com.jdojo.intruder模組的成員。 在main()方法的throws子句中添加了一些異常,以保持邏輯簡單。 在實際的程式中,在try-catch塊中處理它們。

// TestNonOpen.java
package com.jdojo.intruder;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class TestNonOpen {
    public static void main(String[] args)
            throws IllegalAccessException, IllegalArgumentException,
            NoSuchMethodException, ClassNotFoundException,
            InvocationTargetException, InstantiationException,
            NoSuchFieldException {
        String className = "com.jdojo.address.Address";
        // Get the class reference
        Class<?> cls = Class.forName(className);
        // Get the no-args constructor
        Constructor constructor = cls.getConstructor();
        // Create an Object of the Address class
        Object address = constructor.newInstance();
        // Call the getLine1() method to get the line1 value
        Method getLine1Ref = cls.getMethod("getLine1");
        String line1 = (String)getLine1Ref.invoke(address);
        System.out.println("Using method reference, Line1: " + line1);
        // Use the private line1 instance variable to read its value
        Field line1Field = cls.getDeclaredField("line1");
        line1Field.setAccessible(true);
        String line11 = (String)line1Field.get(address);
        System.out.println("Using private field reference, Line1: " + line11);
    }
}

使用以下命令編譯TestNonOpen類:

C:Java9revealed> javac -d com.jdojo.intruder\build\classes
com.jdojo.intruder\src\com\jdojo\intruder\TestNonOpen.java

TestNonOpen類編譯正常。 請注意,它使用深層反射訪問Address類,編譯器不知道此類不允許讀取Address類及其私有欄位。 現在嘗試執行TestNonOpen類:

C:Java9revealed> java --module-path com.jdojo.address\dist;com.jdojo.intruder\build\classes
--add-modules com.jdojo.address
--module com.jdojo.intruder/com.jdojo.intruder.TestNonOpen

會出現以下錯誤資訊:

Using method reference, Line 1: 1111 Main Blvd.
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String com.jdojo.address.Address.line1 accessible: module com.jdojo.address does not "opens com.jdojo.address" to module com.jdojo.intruder
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:207)
        at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:171)
        at java.base/java.lang.reflect.Field.setAccessible(Field.java:165)
        at com.jdojo.intruder/com.jdojo.intruder.TestNonOpen.main(TestNonOpen.java:35)

使用--add-modules選項將com.jdojo.address模組新增到預設的根模組中。 即使com.jdojo.intruder模組沒有讀取com.jdojo.address模組,也可以例項化Address類。 有兩個原因:

  • com.jdojo.address模組匯出包含Address類的com.jdojo.address包。 因此,其他模組可訪問Address類,只要其他模組讀取com.jdojo.address模組即可。
  • Java 反射 API假定所有反射操作都是可讀性的。 該規則假設com.jdojo.intruder模組讀取com.jdojo.address模組,即使在其模組宣告中,com.jdojo.intruder模組未讀取com.jdojo.address模組。 如果要在編譯時使用com.jdojo.address包中的型別,例如,宣告Address類型別的變數,則com.jdojo.intruder模組必須在它宣告或命令列中讀取com.jdojo.address模組。

輸出顯示TestNonOpen類能夠呼叫Address類的public getLine1()方法。 但是,當它嘗試訪問私有line1欄位時,丟擲異常。 回想一下,如果模組匯出了型別,其他模組可以使用反射來訪問該型別的公共成員。 對於其他模組訪問型別的私有成員,包含該型別的包必須是開啟的。 com.jdojo.address包未開啟。 因此,com.jdojo.intruder模組無法訪問Address類的私有line1欄位。 為此,可以使用--add-opens選項將com.jdojo.address包開啟到com.jdojo.intruder模組中:

C:Java9revealed> java --module-path com.jdojo.address\dist;com.jdojo.intruder\build\classes
--add-modules com.jdojo.address
--add-opens com.jdojo.address/com.jdojo.address=com.jdojo.intruder
--module com.jdojo.intruder/com.jdojo.intruder.TestNonOpen

輸出結果為:

Using method reference, Line1: 1111 Main Blvd.
Using private field reference, Line1: 1111 Main Blvd.

現在是時候使用--permit-illegal-access選項了。 我們試試從類路徑執行TestNonOpen類,如下所示:

C:\Java9Revealed>java --module-path com.jdojo.address\dist
--class-path com.jdojo.intruder\build\classes
--add-modules com.jdojo.address com.jdojo.intruder.TestNonOpen

輸出結果為:

Using method reference, Line1: 1111 Main Blvd.
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String com.jdojo.address.Address.line1 accessible: module com.jdojo.address does not "opens com.jdojo.address" to unnamed module @9f70c54
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
        at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:175)
        at java.base/java.lang.reflect.Field.setAccessible(Field.java:169)
        at com.jdojo.intruder.TestNonOpen.main(TestNonOpen.java:34)

從輸出可以看出,由於它位於類路徑上,載入到未命名模組中的TestNonOpen類能夠在com.jdojo.address模組中讀取匯出的型別及其公共方法。 但是,它無法訪問私有例項變數。 可以使用--permit-illegal-access選項修復此問題,如下所示:

C:\Java9Revealed>java --module-path com.jdojo.address\dist
--class-path com.jdojo.intruder\build\classes
--add-modules com.jdojo.address
--permit-illegal-access com.jdojo.intruder.TestNonOpen

輸出結果為:

WARNING: --permit-illegal-access will be removed in the next major release
Using method reference, Line1: 1111 Main Blvd.
WARNING: Illegal access by com.jdojo.intruder.TestNonOpen (file:/C:/Java9Revealed/com.jdojo.intruder/build/classes/) to field com.jdojo.address.Address.line1 (permitted by --permit-illegal-access)
Using private field reference, Line1: 1111 Main Blvd.

請注意,由於--permit-illegal-access選項的警告和TestNonOpen類的訊息的都會混合在輸出中。

四. 使用JAR的Manifest屬性

可執行的JAR是一個JAR檔案,可用於使用如下所示的-jar選項直接執行Java應用程式:

java -jar myapp.jar

這裡,myapp.jar被稱為可執行JAR。其MANIFEST.MF檔案中的可執行JAR包含一個名為Main-Class的屬性,其值是java命應執行的主類的完全限定名。回想一下,還有其他種類的JAR,如模組化JAR和多版本JAR。 JAR基於哪種JAR無關緊要;可執行JAR僅在使用-jar選項用於啟動應用程式的方式的上下文中定義。

考慮現有應用程式作為可執行JAR。假設應用程式使用深層反射來訪問JDK內部API。它在JDK 8中工作正常。希望在JDK 9上執行可執行檔案JAR。JDK 9中的JDK內部API已封裝。現在,必須使用--add-exports-add-opens命令列選項協同-jar選項來執行相同的可執行檔案JAR。在JDK 9中使用新的命令列選項提供了一個解決方案。然而,對於可執行JAR的終端使用者來說,這是不方便的。要使用JDK 9,他們需要知道所需要使用的新的命令列選項。為了緩解這種遷移,JDK 9中添加了可執行JAR的MANIFEST.MF檔案的兩個新屬性:

Add-Exports
Add-Opens

這些屬性將新增到manifest檔案的主要部分。 它們是--add-exports--add-opened兩個命令列選項的對應。 使用這些屬性有一個區別。 它們匯出和開啟模組的包給所有的未命名模組。 因此,可以指定源模組列表,它們的包不必將目標模組指定為這些屬性的值。 換句話說,在manifest檔案中,可以匯出或開啟包給所有的未命名模組,也可以不開啟所選模組。 這些屬性的值是以分隔開的模組名/包名稱對的空格分隔的列表。 這是一個例子:

Add-Exports: m1/p1 m2/p2 m3/p3 m1/p1

該條目將將模組m1中的軟體包p1,模組m2中的軟體包p2,模組m3中的軟體包p3匯出到所有未命名的模組。 解析manifest檔案的規則是寬鬆的,並允許重複。 請注意該值中的重複條目m1/p1。 在執行時,這些包將被匯出到所有未命名的模組。

來看一個例子。 這個例子很簡單,java.lang.Long類包含一個名為serialVersionUID私有靜態欄位,宣告如下:

private static final long serialVersionUID = 4290774380558885855L;

下面包含使用深層反射訪問Long.serialVersionUID欄位的TestManifestAttributes類的程式碼。 該類在com.jdojo.intruder模組中。 現有應用程式不使用模組,它們將使用JDK版本8或更低版本開發。 但是,對於這個例子,它沒有任何區別。

// TestManifestAttributes.java
package com.jdojo.intruder;
import java.lang.reflect.Field;
public class TestManifestAttributes {
    public static void main(String[] args) throws NoSuchFieldException,
                     IllegalArgumentException, IllegalAccessException {
        Class<Long> cls = Long.class;
        Field svUid = cls.getDeclaredField("serialVersionUID");
        svUid.setAccessible(true);
        long svUidValue = (long)svUid.get(null);
        System.out.println("Long.serialVersionUID=" + svUidValue);
    }
}

TestManifestAttributes類編譯沒有任何錯誤。 我們把它打包成一個可執行的JAR。 如下顯示了在JDK 9之前可執行的JAR中的MANIFEST.MF檔案的內容。MANIFEST.MF檔案保持在JAR檔案根目錄下的META-INF目錄中。

Manifest-Version: 1.0
Main-Class: com.jdojo.intruder.TestManifestAttributes

以下命令將建立名為com.jdojo.intruder.jar的可執行檔案JAR:可執行檔案JAR將被放置在com.jdojo.intruder\dist目錄中。 或者,可以從NetBeans IDE中清理並構建com.jdojo.intruder專案,以建立此JAR。

C:\Java9Revealed>jar --create --file com.jdojo.intruder\dist\com.jdojo.intruder.jar
--manifest=com.jdojo.intruder\src\META-INF\MANIFEST.MF
-C com.jdojo.intruder\build\classes.

現在執行可執行檔案JAR:

C:\Java9Revealed>java -jar com.jdojo.intruder\dist\com.jdojo.intruder.jar

會出現以下錯誤資訊:

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private static final long java.lang.Long.serialVersionUID accessible: module java.base does not "opens java.lang" to unnamed module @224aed64
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:207)
        at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:171)
        at java.base/java.lang.reflect.Field.setAccessible(Field.java:165)
        at com.jdojo.intruder.TestManifestAttributes.main(TestManifestAttributes.java:10)

執行時錯誤表明應用程式無法訪問私有靜態serialVersionUID,因為java.base模組中的java.lang包未開啟。我們先試試--add-opens這個選項:

C:\Java9Revealed>java --add-opens java.base/java.lang=ALL-UNNAMED
-jar com.jdojo.intruder\dist\com.jdojo.intruder.jar

輸出資訊如下:

Long.serialVersionUID=4290774380558885855

此命令工作正常,並驗證命令列選項是這種情況下的解決方案。 我們使用MANIFEST.MF檔案中的Add-Opens屬性來修復此錯誤,如下所示。

Manifest-Version: 1.0
Main-Class: com.jdojo.intruder.TestManifestAttributes
Add-Opens: java.base/java.lang

使用相同的命令重新建立可執行檔案JAR並執行它:

C:\Java9Revealed>java -jar com.jdojo.intruder\dist\com.jdojo.intruder.jar

輸出結果為:

Long.serialVersionUID=4290774380558885855

應用程式執行正常。 如果JAR不用作可執行JAR,我們來驗證是否忽略Add-Opens屬性。 怎麼驗證這個? 通過將可執行JAR放置在類路徑或模組路徑上來執行應用程式,並且期望執行時發生錯誤。 請注意,能夠在模組路徑上執行此應用程式,因為正在使用JDK 9並在JAR中包含模組描述。 對於較舊的應用程式,只有一個選項 —— 從類路徑執行它。 以下命令從類路徑執行應用程式:

C:\Java9Revealed>java --class-path com.jdojo.intruder\dist\com.jdojo.intruder.jar com.jdojo.intruder.TestManifestAttributes

會出現以下錯誤:

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private static final long java.lang.Long.serialVersionUID accessible: module java.base does not "opens java.lang" to unnamed module @17ed40e0
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:207)
        at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:171)
        at java.base/java.lang.reflect.Field.setAccessible(Field.java:165)
        at com.jdojo.intruder.TestManifestAttributes.main(TestManifestAttributes.java:10)

如果要使用類路徑執行此應用程式,如何解決此錯誤? 使用--add-open命令列選項來修復它:

C:\Java9Revealed>java --add-opens java.base/java.lang=ALL-UNNAMED
--class-path com.jdojo.intruder\dist\com.jdojo.intruder.jar com.jdojo.intruder.TestManifestAttributes

五. 總結

JDK 9的主要目標之一是將型別和資源封裝在模組中,並僅匯出其他模組要訪問其公共型別的軟體包。 有時,可能需要打破模組指定的封裝,以啟用白盒測試或使用不受支援的JDK內部API或庫。 這可以通過在編譯時和執行時使用非標準命令列選項來實現。 具有這些選項的另一個原因是向後相容性。

JDK 9提供了兩個命令列選項--add-exports-add-opened,可以在模組宣告中定義封裝。 --add-exports選項允許在模組中將未匯出的包匯出到編譯時和執行時的其他模組。--add-opened選項允許在模組中開啟一個非開放的軟體包到其他模組,以便在執行時進行深度反射。 這些選項的值為/=,其中<source-module>是匯出或開啟<package><target-module-list>,它是以逗號分隔的目標模組名稱列表。 可以使用ALL-UNNAMED作為將所有未命名模組匯出或開啟的目標模組列表的特殊值。

有兩個名為Add-ExportsAdd-Opens的新屬性可用於可執行JAR的manifest 檔案的主要部分。 使用這些屬性的效果與使用類似命名的命令列選項相同,只是這些屬性將指定的包匯出或開啟到所有未命名的模組。 這些屬性的值是以空格分隔的斜體分隔的module-name/package-name對列表。 例如,可執行JAR的manifest檔案的主要部分中的Add-Opens:java.base/java.lang條目將為java.base模組中的所有未命名模組開啟java.lang包。

在測試和除錯過程中,有時需要一個模組讀取另一個模組,其中第一個模組在其宣告中不使用requires語句來讀取第二個模組。 這可以使用--add-reads命令列選項來實現,該選項的值以<source-module>=<target-module-list>的形式指定。<source-module>是其定義被更新以讀取在<target-module-list>中指定的模組列表的模組,該模組是目標模組名稱的逗號分隔列表。 目標模組列表的ALL-UNNAMED的特殊值使得源模組讀取所有未命名的模組。