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。 每個模塊語句都有一個命令行選項。 對於exports opens 語句,可以在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的特殊值使得源模塊讀取所有未命名的模塊。


免責聲明!

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



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