Java 9 揭秘(4. 模塊依賴)


文 by / 林本托

Tips
做一個終身學習的人。

Java 9

在此章節中,主要學習以下內容:

  • 如何聲明模塊依賴
  • 模塊的隱式可讀性意味着什么以及如何聲明它
  • 限定導出(exports)與非限定導出之間的差異
  • 聲明模塊的運行時可選依賴關系
  • 如何打開整個模塊或其選定的軟件包進行深層反射
  • JDK 9中的訪問類型
  • 跨模塊分割包
  • 模塊聲明的約束
  • 不同類型的模塊:命名,未命名,顯式,自動,普通和開放的模塊
  • 如何使用javap工具來解析模塊的定義

一. 聲明模塊依賴

假設你現在已經有兩個模塊,分別是:

  • com.jdojo.address:包含Address類
  • com.jdojo.person:包含Person類。

其中,com.jdojo.person模塊想使用com.jdojo.address模塊下的Address類,其模塊圖如下所示:

模塊圖

在NetBeans中,可以創建兩個名為com.jdojo.address和com.jdojo.person的Java項目。 每個項目將包含與項目名稱相同的模塊的代碼。下面包含了com.jdojo.address的模塊聲明和Address類的代碼。

// module-info.java
module com.jdojo.address {
    // Export the com.jdojo.address package
    exports com.jdojo.address;
}
package com.jdojo.address;
public class Address {
    private String line1 = "1111 Main Blvd.";    
    private String city = "Jacksonville";
    private String state = "FL";
    private String zip = "32256";
    public Address() {
    }
    public Address(String line1, String line2, String city,
                   String state, String zip) {
        this.line1 = line1;
        this.city = city;
        this.state = state;
        this.zip = zip;
    }
    // 省略 getter 和 setter 方法
    @Override
    public String toString() {
        return "[Line1:" + line1 + ", State:" + state +
               ", City:" + city + ", ZIP:" + zip + "]";
    }
}

export語句用於將包導出到所有其他模塊或某些命名模塊。 導出的包中的所有公共類型都可以在編譯時和運行時訪問。 在運行時,可以使用反射來訪問公共類型的公共成員。 即使在這些成員上使用了setAccessible(true)方法,公共類型的非公開成員也無法使用反射。 exports語句的一般語法如下所示:
exports <package>;

該語句將<package>中的所有公共類型導出到所有其他模塊。 也就是說,讀取此模塊的任何模塊都將能夠使用<package>中的所有公共類型。

com.jdojo.address模塊導出com.jdojo.address包,因此Address類可以由其他模塊使用,它是公共的,也可以在com.jdojo.address包中使用。所以可以在com.jdojo.person模塊中使用Address類。
下列包含com.jdojo.person模塊的模塊聲明和Person類的代碼。

// module-info.java
module com.jdojo.person {
    // Read the com.jdojo.address module
    requires com.jdojo.address;
    // Export the com.jdojo.person package
    exports com.jdojo.person;
}
The Module Declaration for the com.jdojo.person Module
// Person.java
package com.jdojo.person;
import com.jdojo.address.Address;
public class Person {
    private long personId;
    private String firstName;
    private String lastName;
    private Address address = new Address();
    public Person(long personId, String firstName, String lastName) {
        this.personId = personId;
        this.firstName = firstName;
        this.lastName = lastName;
    }
    public long getPersonId() {
        return personId;
    }
    public void setPersonId(long personId) {
        this.personId = personId;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public Address getAddress() {
        return address;
    }
    public void setAddress(Address address) {
        this.address = address;
    }
    @Override
    public String toString() {
        return "[Person Id:" + personId + ", First Name:" + firstName +
               ", Last Name:" + lastName + ", Address:" + address + "]";
    }
}

Person類在com.jdojo.person模塊中,它使用com.jdojo.address模塊中的Address類型中的字段。 這意味着com.jdojo.person模塊讀取com.jdojo.address模塊。 這通過com.jdojo.person模塊聲明中的requires語句指示:

// Read the com.jdojo.address module
requires com.jdojo.address;

一個require語句用於指定一個模塊對另一個模塊的依賴。 如果模塊讀取另一個模塊,則第一個模塊在其聲明中需要有一個require語句。 require語句的一般語法如下:
requires [transitive] [static] <module>;

這里,<module>是當前模塊讀取的另一個模塊的名稱。 transitive 和static修飾符都是可選的。 如果存在static修飾符,則<module>模塊在編譯時是必需的,但在運行時是可選的。 transitive 修飾符意味着讀取當前模塊的模塊隱含地讀取<module>模塊。

每個模塊都隱式讀取java.base模塊。 如果一個模塊沒有聲明讀取java.base模塊,編譯器將添加一個require語句,將java.base模塊讀取為模塊聲明。 名為com.jdojo.common的模塊的以下兩個模塊聲明是相同的:

// Declaration #1
module com.jdojo.common {
    // Compiler will add a read to the java.base module
}
// Declaration #2
module com.jdojo.common {
    // Add a read to the java.base module explicitly
    requires java.base;
}

com.jdojo.person模塊的聲明包含一個require語句,這意味着在編譯時和運行時都需要com.jdojo.address模塊。 編譯com.jdojo.person模塊時,必須在模塊路徑中包含com.jdojo.address模塊。 如果使用NetBeans IDE,可以在模塊路徑中包含NetBeans項目或模塊化JAR。 右鍵單擊NetBeans中的com.jdojo.person項目,然后選擇“屬性”。具體如下所示:

在NetBeans中設置項目的模塊路徑

選擇要添加到模塊路徑的NetBeans項目

最后點擊“確定”按鈕。

com.jdojo.person模塊還導出com.jdojo.person包,因此該包中的公共類型(例如Person類)也可以被其他模塊使用。

接下來,我們建立一個包含main方法的類:

// Main.java
package com.jdojo.person;
import com.jdojo.address.Address;
public class Main {
    public static void main(String[] args) {
        Person john = new Person(1001, "John", "Jacobs");
        String fName = john.getFirstName();
        String lName = john.getLastName();
        Address addr = john.getAddress();
        System.out.printf("%s %s%n", fName, lName);
        System.out.printf("%s%n", addr.getLine1());
        System.out.printf("%s, %s %s%n", addr.getCity(),
                          addr.getState(), addr.getZip());
    }
}

運行此類,得到如輸出:

John Jacobs
1111 Main Blvd.
Jacksonville, FL 32256

此時,還可以使用命令提示符運行此示例。 需要將編譯的分解目錄或com.jdojo.person和com.jdojo.address模塊的模塊化JAR包含到模塊路徑中。 以下命令使用兩個NetBeans項目下的build\classes目錄中編譯的類:

C:\Java9Revealed>java --module-path
com.jdojo.person\build\classes;com.jdojo.address\build\classes
--module com.jdojo.person/com.jdojo.person.Main

構建包含模塊的NetBeans項目時,模塊的模塊化JAR存儲在NetBeans項目的dist目錄中。 當構建com.jdojo.person項目時,它將在C:\Java9Revealed\com.jdojo.person\dist目錄中創建一個com.jdojo.person.jar文件。 當在NetBeans中構建項目時,它還會重建當前項目所依賴的所有項目。 對於此示例,構建com.jdojo.person項目也將重建com.jdojo.address項目。 構建com.jdojo.person模塊后,可以使用以下命令運行此示例:

C:\Java9Revealed>java --module-path
com.jdojo.person\dist;com.jdojo.address\dist
--module com.jdojo.person/com.jdojo.person.Main

二. 隱含訪問

如果模塊可以讀取沒有第一個模塊的另一個模塊,包括在其聲明中包含一個require語句來讀取第二個模塊,可以說第一個模塊隱含地讀取第二個模塊。 每個模塊都隱式讀取java.base模塊。 隱式讀取不限於java.base模塊。 模塊也可以隱式讀取另一個模塊,而不是java.base模塊。 在展示如何向模塊添加隱式可讀性之前,先構建一個示例,看看為什么我們需要這個功能。
在上一節中,創建了兩個名為com.jdojo.address和com.jdojo.person的模塊,其中第二個模塊使用以下聲明讀取第一個模塊:

module com.jdojo.person {
    requires com.jdojo.address; 
    ...    
}

com.jdojo.person模塊中的Person類引用com.jdojo.address模塊中的Address類。 讓我們創建另一個名為com.jdojo.person.test的模塊,它讀取com.jdojo.person模塊。 模塊聲明如下所示。

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

需要將com.jdojo.person項目添加到com.jdojo.person.test項目的模塊路徑。 否則,編譯代碼將生成以下錯誤:

C:\Java9Revealed\com.jdojo.person.test\src\module-info.java:3: error: module not found: com.jdojo.person
    requires com.jdojo.person;
1 error

然后,在com.jdojo.person.test項目中添加主類。

package com.jdojo.person.test;

import com.jdojo.person.Person;

public class Main {
    public static void main(String[] args) {
        Person john = new Person(1001, "John", "Jacobs");
        // Get John's city and print it
        String city = john.getAddress().getCity();
        System.out.printf("John lives in %s%n", city);
    }
}

上面的代碼會出現以下錯誤信息:

com.jdojo.person.test\src\com\jdojo\person\test\Main.java:11: error: getCity() in Address is defined in an inaccessible class or interface
        String city = john.getAddress().getCity();

錯誤原因在於com.jdojo.person.test模塊不能訪問Address類。 Address類在com.jdojo.address模塊中,com.jdojo.person.test模塊沒有讀取該模塊。 看代碼,似乎很明顯代碼應該編譯成功。 既然可以訪問使用Address類的Person類; 所以就應該可以使用Address類。 這里,john.getAddress()方法返回一個無權訪問的Address類型的對象。 模塊系統只是對com.jdojo.address模塊定義進行了封裝。 如果模塊想要明確或者隱式地使用Address類,它必須讀取com.jdojo.address模塊。 如何解決? 簡單的答案是com.jdojo.person.test模塊通過將其聲明模塊來讀取com.jdojo.address模塊。

// module-info.java
module com.jdojo.person.test {
    requires com.jdojo.person;
    requires com.jdojo.address; 
}

上面的模塊定義會收到另一個錯誤,該錯誤將聲明未找到com.jdojo.address模塊。 將com.jdojo.address項目添加到com.jdojo.person.test項目的模塊路徑來修復此錯誤。 模塊路徑設置如下所示。

設置模塊路徑

此時,顯示了com.jdojo.person.test模塊的模塊圖。

模塊圖

在com.jdojo.person.test模塊中編譯並運行Main類。 它將打印以下內容:

John lives in Jacksonville

二. 限定導出(Qualified Exports)

假設你正在開發由多個模塊組成的庫或框架。 其中有一個模塊中的包含API,僅供某些模塊內部使用。 也就是說,該模塊中的包不需要導出到所有模塊,而是其可訪問性必須限於幾個命名的模塊。 這可以使用模塊聲明中的限定的export語句來實現。 一般語法如下:

exports <package> to <module1>, <module2>...;

這里,<package>是當前模塊要導出的包的名稱,<module1>,<module2>等是可以讀取當前模塊的模塊的名稱。 以下模塊聲明包含非限定導出和限定導出:

module com.jdojo.common {
    // An unqualified exports statement
    exports com.jdojo.zip;
    // A qualified exports statement
    exports com.jdojo.internal to com.jdojo.address;
}

com.jdojo.common模塊將com.jdojo.zip包導出到所有模塊,而com.jdojo.internal包僅適用於com.jdojo.address模塊。 所有的模塊在讀取com.jdojo.common模塊時都可以讀取com.jdojo.zip模塊中的所有公共類型都。 但是,后一種寫法,那么所有com.jdojo.internal包下的公共類型只能被com.jdojo.address模塊訪問。

你也可以在JDK 9中找到許多有限導出的示例。java.base模塊包含導出到幾個命名模塊的“sun.”和“jdk.”軟件包。 以下命令打印java.base的模塊聲明。 輸出顯示在java.base模塊中使用的一些限定的導出:

c:\>javap jrt:/java.base/module-info.class
Compiled from "module-info.java"
module java.base {
  exports sun.net to jdk.plugin, jdk.incubator.httpclient;
  exports sun.nio.cs to java.desktop, jdk.charsets;
  exports sun.util.resources to jdk.localedata;
  exports jdk.internal.util.xml to jdk.jfr;
  exports jdk.internal to jdk.jfr;
  ...
}

不是JDK 9中的所有內部API都已封裝。在“sun.*” 一些關鍵的內部API包內,例如sun.misc.Unsafe類,由JDK 9之前的開發人員使用,並且仍然可以在JDK 9中訪問。這些包已經被放置在jdk中。 以下命令打印jdk.unsupported模塊的模塊聲明:

C:\Java9Revealed>javap jrt:/jdk.unsupported/module-info.class
Compiled from "module-info.java"
module jdk.unsupported@9-ea {
  requires java.base;
  exports sun.misc;
  exports com.sun.nio.file;
  exports sun.reflect;
  opens sun.misc;
  opens sun.reflect;
}

三. 可選依賴

模塊系統在編譯時以及運行時驗證模塊的依賴關系。 有時希望在編譯時模塊依賴性是必需的,但在運行時是可選的。

你在開發一個庫時,如果一個特定的模塊在運行時可執行更好的庫。否則,它將回到另一個模塊,使其執行不到最佳的庫。但是,庫是根據可選模塊進行編譯的,如果可選模塊不可用,則確保不依賴於可選模塊的代碼執行。

另一個例子是導出注解包的模塊。 Java運行時已經忽略不存在的注解類型。 如果程序中使用的注釋在運行時不存在,則注解將被忽略。 模塊依賴關系在啟動時驗證,如果模塊丟失,應用程序將無法啟動。 因此,必須將含有注解包的模塊的模塊依賴性聲明為可選。
您可以通過在require語句中使用static關鍵字聲明可選依賴關系:

requires static <optional-package>;

以下模塊聲明包含對com.jdojo.annotation模塊的可選依賴關系:

module com.jdojo.claim {
    requires static com.jdojo.annotation;
}

允許在require語句中同時使用transitive 和static 修飾符:

module com.jdojo.claim {
    requires transitive static com.jdojo.annotation;
}

如果transitive 和static 修飾符一起使用,則可以按任何順序使用。 以下聲明具有與之前相同的語義:

module com.jdojo.claim {
    requires static transitive com.jdojo.annotation;
}

四. 使用反射機制訪問模塊

Java允許使用反射機制訪問所有成員,包括私有,公共,包和受保護的類型。 需要做的是在成員(Field,Method等)對象上調用setAccessible(true)方法。

當導出一個模塊的包時,其他模塊只能在編譯時靜態訪問導出包中的公共類型和公共/受保護的成員,或者在運行時反射。 有幾個有名的框架,如Spring和Hibernate,它們很大程度上依賴於對應用程序庫中定義的類型的成員的深層反射訪問。

模塊系統的設計人員在設計模塊化代碼的深層反射訪問方面面臨着巨大的挑戰。 允許對導出的包類型的深層反射違反了模塊系統的強封裝的主題。 即使模塊開發人員不想公開模塊的某些部分,它也可以使外部代碼訪問。 另一方面,通過不允許深層反射將會消除Java社區中一些廣泛使用的框架,並且還會破壞依賴於深層反射的許多現有應用程序。 由於這個限制,許多現有應用程序將不會遷移到JDK 9。

經過幾次的設計和實驗迭代,模塊系統設計人員想出了一個變通的解決方案, 目前的設計允許擁有強大的封裝,深層反射訪問,或者兩者一直。 規則如下:

  • 導出的包將允許在編譯時和運行時訪問公共類型及其公共成員。 如果不導出包,則該包中的所有類型都不可訪問其他模塊。 這提供了強大的封裝。
  • 可以打開一個模塊,以便在運行時對該模塊中的所有包中的所有類型進行深層反射。 這樣的模塊稱為開放模塊。
  • 你可以有一個普通的模塊 —— 一個不能進行深層反射的模塊(在運行時打開深度反射的特定軟件包)。 所有其他軟件包(非開放軟件包)都被強力封裝。 允許深度反射的模塊中的軟件包稱為開放軟件包。
  • 有時,可能希望在編譯時訪問包中的類型,以便根據該包中的類型編寫代碼,同時,您也可以在運行時深層反射訪問這些類型。 可以導出並打開相同的包來實現此目的。

1. 開放模塊

聲明開放模塊的語法如下:

open module com.jdojo.model {
    // Module statements go here
}

在這里,com.jdojo.model模塊是一個開放模塊。 其他模塊可以在本模塊中的所有軟件包上對所有類型使用深層反射。 可以在開放模塊中聲明 exports, requires, uses, 和provides語句。 但不能在打開的模塊中再聲明opens語句。 opens語句用於打開特定的包以進行深層反射。 因為開放模塊打開所有的軟件包進行深層反射,所以在開放模塊中不允許再使用open語句。

2. 打開包

打開一個包意味着允許其他模塊對該包中的類型使用深層反射。 可以打開一個包指定給所有其他模塊或特定的模塊列表。
打開一個包到所有其他模塊的打開語句的語法如下:

opens <package>;

這里,<package>可用於深入反射所有其他模塊。 也可以使用限定的open語句打開包到特定模塊:

opens <package> to <module1>, <module2>...;

在這里,<package>僅用於深層反射到<module1>,<module2>等。以下是在模塊聲明中使用opens語句的示例:

module com.jdojo.model {
    // Export the com.jdojo.util package to all modules
    exports com.jdojo.util;
    // Open the com.jdojo.util package to all modules
    opens com.jdojo.util;
    // Open the com.jdojo.model.policy package only to the
    // hibernate.core module
    opens com.jdojo.model.policy to hibernate.core;
}

com.jdojo.model模塊導出com.jdojo.util包,這意味着所有公共類型及其公共成員在編譯時可以訪問,並在運行時進行反射。 第二個語句在運行時打開相同的包進行深度反射。 總而言之,com.jdojo.util包的所有公共類型及其公共成員都可以在編譯時訪問,並且該包允許在運行時深層反射。 第三個語句僅將com.jdojo.model.policy包打包到hibernate.core模塊進行深層反射,這意味着其他模塊在編譯時不能訪問此包的任何類型,而hibernate.core模塊可以訪問所有類型及其成員在運行時進行深度反射。

3. 使用深度反射

在本節中,解釋如何打開模塊和軟件包進行深度反射。 從一個基本的用例開始,然后構建一個例子。 在這個例子中:

  • 展示嘗試使用深層反思進行某些操作的代碼。 通常,代碼將生成錯誤。
  • 解釋錯誤背后的原因。
  • 如何解決錯誤。

在com.jdojo.reflect模塊中包含com.jdojo.reflect包,其中包含一個名為Item的類。下面包含了模塊和類的源代碼。

// module-info.java
module com.jdojo.reflect {
    // No module statements
}
// Item.java
package com.jdojo.reflect;
public class Item {
    static private int s = 10;
    static int t = 20;
    static protected int u = 30;
    static public int v = 40;
}

該模塊不導出任何包,也不打開任何包。 Item類非常簡單。 它包含四個靜態變量,每個類型的訪問修飾符是private,package,protected和public。接下來使用深層反射訪問這些靜態變量。
使用另一個名為com.jdojo.reflect.test的模塊。 聲明如下。 它是一個沒有模塊語句的普通模塊。 也就是說,它沒有依賴關系,除了java.base模塊上的默認值。

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

com.jdojo.reflect.test模塊包含一個名為ReflectTest的類,代碼如下。

// ReflectTest.java
package com.jdojo.reflect.test;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
public class ReflectTest {
    public static void main(String[] args) throws ClassNotFoundException {
        // Get the Class object for the com.jdojo.reflect.Item class
        // which is in the com.jdojo.reflect module
        Class<?> cls = Class.forName("com.jdojo.reflect.Item");
        Field[] fields = cls.getDeclaredFields();
        for (Field field : fields) {
            printFieldValue(field);
        }
    }
    public static void printFieldValue(Field field) {
        String fieldName = field.getName();
        try {                
            // Make the field accessible, in case it is not accessible
            // based on its declaration such as a private field
            field.setAccessible(true);            
            // Print the field's value
            System.out.println(fieldName + " = " + field.get(null));
        } catch (IllegalAccessException | IllegalArgumentException |
                 InaccessibleObjectException e) {
            System.out.println("Accessing " + fieldName +
                               ". Error: " + e.getMessage());
        }
    }
}

ReflectTest類的main()方法中,使用Class.forName()方法來加載com.jdojo.reflect.Item類,並嘗試打印該類的所有四個靜態字段的值。
我們來運行ReflectTest類。它會生成以下錯誤:

Exception in thread "main" java.lang.ClassNotFoundException: com.jdojo.reflect.Item
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:553)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:185)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:486)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:291)
	at com.jdojo.reflect.test/com.jdojo.reflect.test.ReflectTest.main(ReflectTest.java:9)

該錯誤消息表示當嘗試加載com.jdojo.reflect.Item類時拋出ClassNotFoundException異常。 這個錯誤源於另一個問題。
當嘗試加載類時,包含該類的模塊必須為模塊系統所知。 如果在JDK 9之前收到ClassNotFoundException,則表示該類不在類路徑中。 可以將包含該類的目錄或JAR添加到類路徑中,並且該錯誤將被解析。 在JDK 9中,使用模塊路徑找到模塊。 所以,我們在模塊路徑上添加com.jdojo.reflect模塊,然后運行ReflectTest類。 在NetBeans中,您需要使用屬性對話框將com.jdojo.reflect項目添加到com.jdojo.reflect.test模塊的模塊路徑中,如下圖所示。

模塊路徑

也可以使用以下命令運行ReflectTest類,假設已在NetBeans中構建了這兩個項目,並且項目的dist目錄包含模塊化JAR。

C:\Java9Revealed>java
--module-path com.jdojo.reflect\dist;com.jdojo.reflect.test\dist
--module com.jdojo.reflect.test/com.jdojo.reflect.test.ReflectTest

在NetBeans中運行ReflectTest類,並在命令提示符下返回與之前相同的ClassNotFoundException。 所以看起來,將com.jdojo.reflect模塊添加到模塊路徑中沒有幫助。 其實這個步驟有所幫助,但是它只解決了一半的問題。 我們需要理解和解決另一半問題,這就是模塊圖。

JDK 9中的模塊路徑聽起來類似於類路徑,但它們的工作方式不同。 模塊路徑用於在模塊解析期間定位模塊 —— 當模塊圖形被構建和擴充時。 類路徑用於在需要加載類時定位類。 為了提供可靠的配置,模塊系統確保啟動時存在所有必需的模塊依賴關系。 一旦應用程序啟動,所有需要的模塊都將被解析,並且在模塊解析結束后,在模塊路徑中添加更多的模塊不會有幫助。 當運行ReflectTest類時,在模塊路徑上同時運行com.jdojo.reflect和com.jdojo.reflect.test模塊,模塊圖如下所示。
模塊圖

當從模塊運行類時,正如在運行ReflectTest類時所做的那樣 —— 包含主類的模塊是用作根目錄的唯一模塊。 模塊圖包含主模塊所依賴的所有模塊及其依賴關系。 在這種情況下,com.jdojo.reflect.test模塊是默認的根模塊集中的唯一模塊,模塊系統對於com.jdojo.reflect模塊的存在沒有線索,即使模塊被放置在模塊路徑。 需要做什么才能使com.jdojo.reflect模塊包含在模塊圖中? 使用--add-modules命令行VM選項將此模塊添加到默認的根模塊中。 此選項的值是以逗號分隔的要添加到默認的根模塊集的模塊列表:

--add-modules <module1>,<module2>...

下圖顯示了NetBeans中com.jdojo.reflect.test項目的“屬性”對話框,其中使用VM選項可將com.jdojo.reflect模塊添加到默認的根模塊集中。

--add-modules

將com.jdojo.reflect模塊添加到默認的根模塊后,運行時的模塊圖如下所示。

模塊圖

解決com.jdojo.reflect模塊的另一種方法是添加一個require com.jdojo.reflect; 聲明。 這樣,com.jdojo.reflect模塊將被解析為com.jdojo.reflect.test模塊的依賴項。 如果使用此選項,則不需要使用--add-modules選項。

在NetBeans中重新運行ReflectTest類。 還可以使用以下命令來運行它:

C:\Java9Revealed>java
--module-path com.jdojo.reflect\dist;com.jdojo.reflect.test\dist
--add-modules com.jdojo.reflect
--module com.jdojo.reflect.test/com.jdojo.reflect.test.ReflectTest

會得到以下錯誤信息:

Accessing s. Error: Unable to make field private static int com.jdojo.reflect.Item.s accessible: module com.jdojo.reflect does not "opens com.jdojo.reflect" to module com.jdojo.reflect.test
Accessing t. Error: Unable to make field static int com.jdojo.reflect.Item.t accessible: module com.jdojo.reflect does not "opens com.jdojo.reflect" to module com.jdojo.reflect.test
Accessing u. Error: Unable to make field protected static int com.jdojo.reflect.Item.u accessible: module com.jdojo.reflect does not "opens com.jdojo.reflect" to module com.jdojo.reflect.test
Accessing v. Error: Unable to make field public static int com.jdojo.reflect.Item.v accessible: module com.jdojo.reflect does not "exports com.jdojo.reflect" to module com.jdojo.reflect.test

com.jdojo.reflect.Item類已成功加載。當程序試圖在字段上調用setAccessible(true)時,會為每個字段拋出一個InaccessibleObjectException異常。注意輸出中四個錯誤消息的區別。對於s,t和u字段,錯誤消息表示無法訪問它們,因為com.jdojo.reflect模塊未打開com.jdojo.reflect包。對於v字段,錯誤消息指出該模塊不導出com.jdojo.reflect包。不同錯誤消息背后的原因是v字段是公開的,而其他字段是非公開的。要訪問公共字段,需要導出包,這是允許的最小可訪問性。要訪問非公共字段,必須打開該包,這是允許的最大可訪問性。

下面包含com.jdojo.reflect模塊的模塊聲明的修改版本。它導出com.jdojo.reflect包,因此所有公共類型及其公共成員都可以通過外部代碼訪問。

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

重新運行ReflectTest類,會得到以下錯誤信息:

Accessing s. Error: Unable to make field private static int com.jdojo.reflect.Item.s accessible: module com.jdojo.reflect does not "opens com.jdojo.reflect" to module com.jdojo.reflect.test
Accessing t. Error: Unable to make field static int com.jdojo.reflect.Item.t accessible: module com.jdojo.reflect does not "opens com.jdojo.reflect" to module com.jdojo.reflect.test
Accessing u. Error: Unable to make field protected static int com.jdojo.reflect.Item.u accessible: module com.jdojo.reflect does not "opens com.jdojo.reflect" to module com.jdojo.reflect.test
v = 40

如預期的那樣,可以訪問公共的v域的值。 導出包允許僅訪問公共類型及其公共成員。 不能訪問其他非公開字段。 要獲得對Item類的深層反射訪問,解決方案是打開整個模塊或包含Item類的包。 下面包含com.jdojo.reflect模塊的修改版本,它將其聲明為一個開放模塊。 一個開放的模塊在運行時導出所有的軟件包,用於深層反射。

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

再重新運行ReflectTest類,會得到以下信息:

s = 10
t = 20
u = 30
v = 40

輸出顯示可以從com.jdojo.reflect.test模塊訪問所有項目類的所有字段(公共和非公開的)。 也可以通過打開com.jdojo.reflect包而不是打開整個模塊來獲得相同的結果。 com.jdojo.reflect模塊聲明的修改版本,如下所示,實現了這一點。 重新編譯你的模塊,並重新運行ReflectTest類,就像上一步一樣,將獲得相同的結果。

// module-info.java
module com.jdojo.reflect {
    opens com.jdojo.reflect;
}

這個例子基本結束了! 有幾點值得注意:

  • 開放模塊或具有開放包的模塊允許訪問所有類型的成員以深層反射到其他模塊,其他模塊不必聲明對第一個模塊的依賴。 在此示例中,com.jdojo.reflect.test模塊能夠訪問Item類及其成員,而不聲明對com.jdojo.reflect模塊的依賴。 這個規則是為了確保使用深層反射的Hibernate和Spring等框架不必聲明對應用程序模塊的訪問依賴。
  • 如果要在編譯時訪問包的公共API,並希望在運行時使用深層反射訪問相同的包,則可以打開並導出相同的包。 在這個例子中,我們可以在com.jdojo.reflect模塊中導出並打開com.jdojo.reflect包。
  • 如果一個模塊是打開的或者它打開的包,你仍然可以聲明對它們的依賴,但是沒有必要。 此規則有助於遷移到JDK 9。如果你的模塊在其他已知模塊上使用深層反射,則你的模塊應聲明對這些模塊的依賴性,以獲得可靠配置的好處。

我們來看看這些模塊的最終版本。 下面兩個包含這些模塊聲明的修改版本。

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

現在,運行ReflectTest類時,不需要使用--add-modules VM選項。 com.jdojo.reflect模塊將被解析,因為在com.jdojo.reflect.test模塊的模塊聲明中添加了requires com.jdojo.reflect;語句。 下圖顯示了運行ReflectTest類時創建的模塊圖。

模塊圖

五. 訪問類型

在JDK 9之前,有四種訪問類型:

  • public
  • protected
  • <package>
  • private

在JDK 8中,public類型意味着程序的所有部分都可以訪問它。 在JDK 9中,這已經改變了。 public類型可能不是對每個人都公開的。 模塊中定義的public類型可能分為三類:

  • 僅在定義模塊內公開
  • 只針對特定模塊公開
  • 指定所有人公開

如果一個類型在模塊中被定義為public,但是該模塊不導出包含該類型的包,則該類型僅在該模塊中是公開的。 沒有其他模塊可以訪問類型。

如果一個類型在一個模塊中被定義為public,但是該模塊使用一個限定的export來導出包含該類型的包,該類型將只能在有限導出的子句中指定的模塊中訪問。

如果一個類型在模塊中被定義為public,但該模塊使用包含該類型的非限定的導出語句導出該包,該類型將公開給讀取第一個模塊的每個模塊。

六. 分割模塊中的軟件包

將包拆分成多個模塊是不允許的。也就是說,同一個包不能在多個模塊中定義。如果同一個軟件包中的類型在多個模塊中,那么這些模塊應該被組合成一個模塊,或者你需要重命名軟件包。有時,您可以成功編譯這些模塊並收到運行時錯誤;其他時候,會收到編譯時錯誤。

如果兩個名為M和N的模塊定義了相同的軟件包P,則不能存在模塊Q,使得M和N模塊中的軟件包P都可以訪問。換句話說,多個模塊中的相同軟件包不能同時讀取模塊。否則,會發生錯誤。請考慮以下代碼片段:

// Test.java
package java.util;
public class Test {
}

如果您在JDK 9中編譯Test類作為模塊的一部分將收到以下錯誤:

error: package exists in another module: java.base
package java.util;

如果你在這個名為M的模塊中有這個類,那么編譯時錯誤就是說這個模塊中的模塊M以及java.base模塊都可以讀取java.util包。 必須將此類的包更改為任何可觀察模塊中不存在的名稱。

七. 模塊聲明中的約束

聲明模塊有幾個約束。 如果違反了這些規定,將在編譯時或啟動時收到錯誤:

  • 模塊圖不能包含循環依賴。也就是說,兩個模塊不能彼此讀取。 如果他們這樣做,他們應該是一個模塊,而不是兩個。 請注意,可以通過以編程方式添加可讀性或使用命令行選項在運行時具有循環依賴性。
  • 模塊聲明不支持模塊版本。 需要使用jar工具或其他一些工具(如javac)將模塊的版本添加為類文件屬性。
  • 模塊系統沒有子模塊的概念。 也就是說,com.jdojo.person和com.jdojo.person.client是兩個單獨的模塊; 第二個不是第一個的子模塊。

八. 模塊的類型

Java已經存在了20多年,舊的和新的應用程序將繼續使用未被模塊化或永遠不會被模塊化的庫。 如果JDK 9迫使所有人將其應用程序模塊化,JDK 9可能不會被廣泛采用。 JDK 9設計師保持向后兼容性。 可以通過以自己的速度調整應用程序或通過決定不通過在JDK 9中運行現有應用程序來模塊化來采用JDK 9。在大多數情況下,在JDK 8或更早版本中工作的應用程序將繼續工作 JDK 9沒有任何變化。 為了簡化遷移,JDK 9定義了四種類型的模塊:

  • 普通模塊(Normal modules)
  • 開發模塊(Open modules)
  • 自動模塊(Automatic modules)
  • 未命名模塊(Unnamed modules)

實際上,將會遇到六個不同類型的模塊的術語,對於JDK 9的初學者來說,此處最為模糊。 其他兩種類型的模塊用於傳達這四種類型的模塊的更廣泛的類別。 下圖顯示了所有模塊類型的圖示。

模塊的類型

在描述模塊的主要類型之前,先簡要介紹上圖表示的模塊類型。

  • 一個模塊是代碼和數據的集合。
  • 基於模塊是否具有名稱,模塊可以是命名模塊或未命名模塊。
  • 沒有其他類別的未命名模塊。
  • 當模塊具有名稱時,可以在模塊聲明中明確指定名稱,或者可以自動(或隱式地)生成名稱。 如果名稱在模塊聲明中明確指定,則稱為顯式模塊。 如果名稱由模塊系統通過讀取模塊路徑上的JAR文件名生成,則稱為自動模塊。
  • 如果不使用open修飾符的情況下聲明模塊,則稱為普通模塊。
  • 如果使用open修飾符聲明模塊,則稱為開放模塊。

基於這些定義,開放模塊也是顯式模塊和命名模塊。 自動模塊是一個命名模塊,因為它具有自動生成的名稱,但它不是顯式模塊,因為它在模塊系統在編譯時和運行時被隱式聲明。 以下小節介紹這些模塊類型。

1. 普通模塊

使用模塊聲明明確聲明而不使用opem修飾符的模塊始終被賦予一個名稱,它被稱為普通模塊或簡化模塊。 到目前為止,你一直在使用大多數普通的模塊。 目前一直將普通模塊稱為模塊,后面繼續在這個意義上使用這個術語,除非需要區分四種類型的模塊。 默認情況下,普通模塊中的所有類型都被封裝。 普通模塊的一個例子如下:
module a.normal.module {
// Module statements go here
}

2. 開放模塊

如果模塊聲明包含open修飾符,則該模塊被稱為開發模塊。 開放模塊的一個例子如下:

open module a.open.module {
    // Module statements go here
}

3. 自動模塊

為了向后兼容,查找類型的類路徑機制仍然可以在JDK 9中使用。可以選擇將JAR放在類路徑,模塊路徑和兩者的組合上。 請注意,可以在模塊路徑和類路徑上放置模塊化JAR以及JAR。

將JAR放在模塊路徑上時,JAR被視為一個模塊,稱為自動模塊。 名稱自動模塊是從模塊從JAR自動定義的事實得出的,不通過添加module-info.class文件來顯式聲明模塊。 自動模塊有一個名稱。 自動模塊的名稱是什么? 它讀取哪些模塊以及導出哪些軟件包?

自動模塊其實也是一個有名字的模塊。 其名稱和版本由JAR文件的名稱派生,對應以下規則:

  • 刪除JAR文件的.jar擴展名。 如果JAR文件名是com.jdojo.intro-1.0.jar,則此步驟將刪除.jar擴展名,並通過以下步驟使用com.jdojo.intro-1.0來推導出模塊的名稱及其版本。
  • 如果名稱以連字符后跟至少一個數字(也可后跟一個點),則模塊名稱將從最后一個連字符之前的名稱部分派生。 如果它可以被解析為有效的版本,連字符后面的部分被分配為模塊的版本。 在此示例中,模塊名稱將從com.jdojo.intro派生。 版本將派生為1.0。
  • 名稱部分中的每個非字母數字字符都被替換為一個點,並且在所得到的字符串中,用一個點替換兩個連續的點。 此外,所有前導和后跟的點都被刪除。 在本示例中,我們在名稱部分中沒有任何非字母數字字符,因此模塊名稱為com.jdojo.intro。

按順序應用這些規則可以提供模塊名稱和模塊版本。 在本節結尾處,展示如何使用JAR文件確定自動模塊的名稱。 下面列出了幾個JAR名稱,以及派生的自動模塊名稱和版本。 請注意,該表不顯示JAR文件名中的擴展名.jar。

Jar  名詞 | 模塊名稱 | 模塊版本
---|---
com.jdojo.intro-1.0 | com.jdojo.intro | 1.0
junit-4.10 | junit | 4.10
jdojo-logging1.5.0 | 有錯誤 | 無版本
spring-core-4.0.1.RELEASE | spring.core | 4.0.1.RELEASE
jdojo-trans-api_1.5_spec-1.0.0 | 有錯誤 | 1.0.0
_ | 有錯誤 | 無版本

我們來看看表中的三個奇怪的情況,如果你將JAR放在模塊路徑中,你會收到一個錯誤。 生成錯誤的第一個JAR名稱是jdojo-logging1.5.0。 讓我們應用規則來導出此JAR的自動模塊名稱:

  • JAR名稱中沒有連字符,后面緊跟着一個數字,所以沒有模塊版本。 整個JAR名稱用於導出自動模塊名稱。
  • 所有非字母數字字符都被替換為一個點。 生成的字符串是jdojo.logging1.5.0。 模塊名稱的每個部分都必須是有效的Java標識符。 在這種情況下,5和0是模塊名稱中的兩個部分,它們不是有效的Java標識符。 因此,派生模塊名稱無效。 這是在將此JAR文件添加到模塊路徑時收到錯誤的原因。

生成錯誤的另一個JAR名稱是jdojo-trans-api_1.5_spec-1.0.0。 我們來應用規則來推導出這個JAR的自動模塊名稱:

  • 找到最后一個連字符,之后只有數字和點,並將JAR名稱分為兩部分:jdojo-trans-api_1.5_spec和1.0.0。 第一部分用於派生模塊名稱。 第二部分是模塊版本。
  • 名稱部分中的所有非字母數字字符都將替換為一個點。 生成的字符串是jdojo.trans.api.1.5.spec,它是一個無效的模塊名稱,因為1和5並且不是有效的Java標識符。 這是在將此JAR文件添加到模塊路徑時收到錯誤的原因。

表中的最后一個條目包含一個下划線()作為JAR名稱。 也就是說,JAR文件被命名為.jar。 如果應用規則,下划線將被一個點替換,並且該點將被刪除,因為它是名稱中唯一的字符。 最后一個空字符串,這不是一個有效的模塊名稱。
如果無法從其名稱導出有效的自動模塊名稱,則放置在模塊路徑上的JAR將拋出異常。 例如,模塊路徑上的_.jar文件將導致以下異常:

java.lang.module.ResolutionException: Unable to derive module descriptor for: _.jar

可以使用帶有--describe-module選項的jar命令打印模塊化JAR的模塊描述符,並打印JAR派生的自動模塊名稱的名稱。 對於JAR,它還打印JAR包含的包的列表。 使用命令的一般語法如下:

jar --describe-module --file <path-to-JAR>

以下命令將打印cglib-2.2.2.jar的JAR的自動模塊名稱:

C:\Java9Revealed>jar --describe-module --file lib\cglib-2.2.2.jar
No module descriptor found. Derived automatic module.
module cglib@2.2.2 (automatic)
  requires mandated java.base
  contains net.sf.cglib.beans
  contains net.sf.cglib.core
  contains net.sf.cglib.proxy
  contains net.sf.cglib.reflect
  contains net.sf.cglib.transform
  contains net.sf.cglib.transform.impl
  contains net.sf.cglib.util

該命令打印一條消息,指出它在JAR文件中沒有找到模塊描述符,並從JAR導出自動模塊。 如果使用的名稱不能轉換為有效的自動名稱的JAR(例如cglib.1-2.2.2.jar),則jar命令會打印一條錯誤消息,並提示JAR名稱有什么問題,如下所示:

C:\Java9Revealed>jar --describe-module --file lib\cglib.1-2.2.2.jar
Unable to derive module descriptor for: lib\cglib.1-2.2.2.jar
cglib.1: Invalid module name: '1' is not a Java identifier

一旦知道自動模塊的名稱,其他顯式模塊可以使用require語句讀取它。 以下模塊聲明從模塊路徑上的cglib-2.2.2.jar中讀取名為cglib的自動模塊:

module com.jdojo.lib {
    requires cglib;
    //...
}

要有效使用的自動模塊,必須導出包並讀取其他模塊。 我們來看看關於這個的規則:

  • 自動模塊讀取所有其他模塊。 重要的是要注意,在解析模塊圖之后,會添加自動模塊到所有其他模塊,其他模塊都可以讀取自動模塊。
  • 自動模塊中的所有包都被導出並打開。

這兩個規則基於這樣一個事實,即沒有實際的方法來告訴自動模塊所依賴的其他模塊以及其他模塊的哪些軟件包需要為深層反射進行編譯。

讀取所有其他模塊的自動模塊可能會產生循環依賴關系,這在模塊圖解決后才允許。 在模塊圖解析期間不允許模塊之間的循環依賴。 也就是說,模塊聲明中不能有循環依賴。

自動模塊沒有模塊聲明,因此它們不能聲明對其他模塊的依賴。顯式模塊可以聲明對其他自動模塊的依賴。我們來看一個顯式模塊M讀取自動模塊P,模塊P使用另一個自動模塊Q中T類型的情況。當使用模塊M的主類啟動應用程序時,模塊圖將只包含M和P —— 排除簡單的java.base模塊。解析過程將從模塊M開始,並看到它讀取另一個模塊P。解析過程沒有具體的方法告訴模塊P讀取模塊Q。可以同同時編譯模塊P 和 Q 並放在類路徑上。但是,當您運行此應用程序時,您將收到一個ClassNotFoundException異常。當模塊P嘗試從模塊Q訪問類型時,會出現異常。為了解決此問題,模塊Q必須通過使用--add-modules命令行選項作為根模塊添加到模塊圖中並將Q指定為此選項的值。

以下命令描述了cglib的自動模塊,其模塊聲明是通過將文件放在模塊路徑上,從cglib-2.2.2.jar文件派生的。輸出表示名為cglib的自動模塊導出並打開其所有軟件包。

C:\Java9Revealed>java --module-path lib\cglib-2.2.2.jar
--list-modules cglib
automatic module cglib@2.2.2 (file:///C:/Java9Revealed/lib/cglib-2.2.2.jar)
  exports net.sf.cglib.beans
  exports net.sf.cglib.core
  exports net.sf.cglib.proxy
  exports net.sf.cglib.reflect
  exports net.sf.cglib.transform
  exports net.sf.cglib.transform.impl
  exports net.sf.cglib.util
  requires mandated java.base
  opens net.sf.cglib.transform
  opens net.sf.cglib.transform.impl
  opens net.sf.cglib.beans
  opens net.sf.cglib.util
  opens net.sf.cglib.reflect
  opens net.sf.cglib.core
  opens net.sf.cglib.proxy

4. 未命名模塊

可以將JAR和模塊化JAR放在類路徑上。 當類型加載並且在任何已知模塊中找不到其包時,模塊系統會嘗試從類路徑加載類型。 如果在類路徑上找到該類型,它將由類加載器加載,並成為該類加載器的一個名為unreamed模塊的模塊成員。 每個類加載器定義一個未命名的模塊,其成員是從類路徑加載的所有類型。 一個未命名的模塊沒有名稱,因此顯式模塊不能使用require語句來聲明對它的依賴。 如果有明確的模塊需要使用未命名模塊中的類型,則必須通過將JAR放置在模塊路徑上,將未命名模塊的JAR用作自動模塊。

在編譯時嘗試從顯式模塊訪問未命名模塊中的類型是一個常見的錯誤。 這根本不可能,因為未命名的模塊沒有名稱,顯式模塊需要一個模塊名稱才能在編譯時讀取另一個模塊。 自動模塊作為顯式模塊和未命名模塊之間的橋梁,如下所示。 顯式模塊可以使用require語句訪問自動模塊,自動模塊可以訪問未命名的模塊。

橋梁

未命名的模塊沒有名稱。 這並不意味着未命名的模塊的名稱是空字符串,“未命名”,或空值。 模塊的以下聲明無效:

module some.module {
    requires "";        // A compile-time error
    requires "unnamed"; // A compile-time error
    requires unnamed;   // A compile-time error unless you have a named
                        // module whose name is unnamed
    requires null;      // A compile-time error
}

未命名的模塊讀取其他模塊,導出和打開它自己的包給其他模塊,使用以下規則:

  • 一個未命名的模塊讀取每個其他模塊。 因此,未命名的模塊可以訪問所有模塊中所有導出包中的公共類型,包括平台模塊。 該規則使得使用Java SE 8中編譯和運行的類路徑的應用程序可以繼續在Java SE 9中編譯和運行,前提是它們只使用標准的,不被棄用的Java SE API。
  • 一個未命名的模塊打開其本身的所有包給其他模塊使用。 因此,顯式模塊可能在運行時使用反射訪問未命名模塊中的類型。
  • 一個未命名的模塊導出其所有包。 在編譯時,顯式模塊無法讀取未命名的模塊。 模塊圖解析后,所有自動模塊可以讀取未命名的模塊。

Tips
未命名模塊可能包含一個包,此包被一個命名模塊導出。 在這種情況下,未命名模塊中的包將被忽略。

我們來看看使用未命名模塊的兩個例子。 在第一個例子中,普通模塊將使用反射訪問未命名的模塊。普通模塊在編譯時無法訪問未命名的模塊。 在第二個例子中,一個未命名的模塊訪問一個普通模塊。

九. 普通模塊到未命名模塊

不必聲明一個未命名的模塊。 若果要有一個未命名的模塊,需要在類路徑上放置一個JAR或一個模塊化JAR。 通過將其模塊化JAR放置在類路徑上,將com.jdojo.reflect模塊重用為未命名模塊。

下列兩段代碼分別包含名為com.jdojo.unnamed.test的模塊和模塊中的Main類的模塊聲明。 在main()方法中,該類嘗試加載com.jdojo.reflect.Item類並讀取其字段。 為了保持代碼簡單,直接在main方法添加了一個throws子句。

// module-info.com
module com.jdojo.unnamed.test {
    // No module statements
}
// Main.java
package com.jdojo.unnamed.test;
import java.lang.reflect.Field;
public class Main {    
    public static void main(String[] args) throws Exception {
        Class<?> cls = Class.forName("com.jdojo.reflect.Item");
        Field[] fields = cls.getDeclaredFields();
        for (Field field : fields) {            
            field.setAccessible(true);            
            System.out.println(field.getName() + " = " +
                               field.get(null));
        }
    }    
}

在NetBeans中,com.jdojo.unnamed.test模塊中的主類,將com.jdojo.reflect項目添加到com.jdojo.unnamed.test項目的類路徑中,如下圖所示。

Classpath

要運行Main類,使用NetBeans或以下命令。 在運行命令之前,請確保同時構建project-com.jdojo.reflect和com.jdojo.unnamed.test。

C:\Java9Revealed>java --module-path com.jdojo.unnamed.test\dist
--class-path com.jdojo.reflect\dist\com.jdojo.reflect.jar
--module com.jdojo.unnamed.test/com.jdojo.unnamed.test.Main
s = 10
t = 20
u = 30
v = 40

通過將com.jdojo.reflect.jar放在類路徑上,它的Item類將被加載到類加載器的未命名模塊中。 輸出顯示已使用com.jdojo.unnamed.test模塊(這是一個命名模塊)的深層反射,並成功訪問了一個未命名模塊中的Item類。 如果在編譯時嘗試訪問Item類,則會收到編譯時錯誤,因為com.jdojo.unnamed.test模塊不能有可讀取未命名模塊的require語句。

十. 未命名模塊到普通模塊

在本節中,展示如何從未命名的模塊訪問命名模塊中的類型。 在NetBeans中,創建一個Java項目,項目名稱為com.jdojo.unnamed。 這不是一個模塊化的項目。 它不包含一個包含模塊聲明的module-info.java文件。 它是在JDK 8中創建的Java項目。將項目添加到項目中,如下所示。 該類使用com.jdojo.reflect包中的Item類,它是一個名為com.jdojo.reflect的現有項目的成員,它包含一個模塊。

// Main.java
package com.jdojo.unnamed;
import com.jdojo.reflect.Item;
public class Main {
    public static void main(String[] args) {
        int v = Item.v;
        System.out.println("Item.v = " + v);
    }
}

主類沒有編譯成功, 它不知道Item類在哪里。 我們將com.jdojo.reflect項目添加到com.jdojo.unnamed項目的模塊路徑中,

模塊路徑

嘗試編譯com.jdojo.unnamed.Main類會生成以下錯誤:

C:\Java9Revealed\com.jdojo.unnamed\src\com\jdojo\unnamed\Main.java:4: error: package com.jdojo.reflect is not visible
import com.jdojo.reflect.Item;
  (package com.jdojo.reflect is declared in module com.jdojo.reflect, which is not in the module graph)

編譯時錯誤表明,Main類不能導入com.jdojo.reflect包,因為它不可見。 括號中的消息為您提供了解決錯誤的實際原因和提示。 您將com.jdojo.reflect模塊添加到模塊路徑。 但是,模塊沒有添加到模塊圖中,因為沒有其他模塊聲明依賴它。 您可以通過使用--add-modules編譯器選項將com.jdojo.reflect模塊添加到默認的根模塊中來解決此錯誤,現在,com.jdojo.unnamed.Main類將編譯好。

添加到默認模塊根集合

重新運行 Main 類,得到以下錯誤:

Exception in thread "main" java.lang.NoClassDefFoundError: com/jdojo/reflect/Item
        at com.jdojo.unnamed.Main.main(Main.java:8)
Caused by: java.lang.ClassNotFoundException: com.jdojo.reflect.Item
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:532)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:186)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:473)
        ... 1 more

運行時錯誤指出找不到com.jdojo.reflect.Item類。 這一次,錯誤並不像你第一次嘗試編譯該類時那么清楚。 但是,這個錯誤的原因是一樣的 —— com.jdojo.reflect模塊在運行時不包含在模塊圖中。 要解決它,需要使用相同的-add-modules選項,但這次為VM選項添加參數。 下圖顯示了如何在NetBeans中添加此選項。

VM選項

再次運行,打印如下信息:

Item.v = 40

輸出顯示,未命名的模塊能夠從命名模塊的導出包訪問公共類型及其公共成員。 注意,無法訪問com.jdojo.unnamed.Main類中的Item類的其他靜態變量(s,t和u),因為它們不是public的。

十一. 遷移到JDK 9的路徑

當將應用程序遷移到JDK 9時,應該考慮到模塊系統提供的兩個好處:強大的封裝和可靠的配置。 你的目標是應用程序由普通模塊組成,除了幾個開放模塊。 牢記應用程序的種類,其他代碼的相互依賴關系,以及不同的配置需求。下面是一些通用的准則,可以幫助你完成遷移過程。

在JDK 9之前,一個有意義的Java應用程序由幾個駐留在三個層中的JAR組成:

  • 開發人員開發的應用程序層中的程序JAR
  • 在類庫層中的類庫 JAR——通常由第三方提供
  • JVM層中的Java運行時的JAR。

JDK 9已經通過將Java運行時JAR轉換為模塊來模塊化。 也就是說,Java運行時由模塊組成。

類庫層主要由放置在類路徑上的第三方JAR組成。 如果要將應用程序遷移到JDK 9,可能無法獲得第三方JAR的模塊化版本。 也無法控制供應商如何將第三方JAR轉換為模塊。 可以將庫JAR放在模塊路徑上,並將其視為自動模塊。

可以選擇完全模塊化你的應用程序代碼。 以下是對模塊類型選擇的選擇 —— 從最不理想到最理想的選擇:

  • 未命名模塊
  • 自動模塊
  • 開放模塊
  • 普通模塊

遷移的第一步是通過將所有應用程序JAR和類庫JAR放入類路徑來檢查您的應用程序是否在JDK 9中運行,而不進行任何代碼的修改。 類路徑上的JAR的所有類型將是未命名模塊的一部分。 在此狀態下的應用程序使用JDK 9,無需任何封裝和可靠的配置。

一旦應用程序在JDK 9中運行,可以開始將應用程序代碼轉換為自動模塊。 自動模塊中的所有軟件包均可打開以進行深度反射訪問,並導出為常規編譯時和運行時訪問的公共類型。 在這個意義上說,它不比未命名的模塊更好,因為它不能提供強大的封裝。 然而,自動模塊可以提供可靠的配置,因為其他顯式模塊可以聲明依賴自動模塊。

還可以選擇將應用程序代碼轉換為提供適度程度的更強封裝的開放模塊:在開放模塊中,所有軟件包都可以進行深度反射訪問,但可以指定哪些軟件包(如果有的話)導出為普通的編譯時和運行時訪問。 顯式模塊還可以聲明對開放模塊的依賴 —— 從而提供可靠配置的好處。

普通模塊提供最強的封裝,可以選擇哪些軟件包(如果有的話)是打開的,導出的,還是兩者。 顯式模塊還可以聲明對開放模塊的依賴,從而提供可靠配置的好處。

下面包含封裝程度和可靠配置的模塊類型列表。

模塊類型 | 強封裝 | 可靠配置
---|---
未命名 | 否 | 否
自動 | 否 | 適度
開放 | 適度 | 是
普通 | 最強 | 最強

十二. 拆解模塊定義

在本節中,使用JDK附帶的javap工具,拆解類文件。該工具在學習模塊系統方面非常有用,特別是在反編譯模塊的描述符中。

此時,com.jdojo.intro模塊有兩個module-info.class文件:一個在mods\com.jdojo.intro目錄中,另一個在lib\com.jdojo下intro-1.0.jar中。當模塊的代碼打包到JAR中時,已為模塊指定了版本和主類。這些信息在哪里去了?它們作為類屬性添加到module-info.class文件中。因此,兩個module-info.class文件的內容不一樣。怎么證明呢?首先在module-info.class文件中打印模塊聲明。可以使用位於JDK_HOME\bin目錄中的javap工具來分析任何類文件中的代碼。可以指定要解析的文件名,URL或類名。以下命令打印模塊聲明:

C:\Java9Revealed>javap mods\com.jdojo.intro\module-info.class
Compiled from "module-info.java"
module com.jdojo.intro {
  requires java.base;
}
C:\Java9Revealed>javap jar:file:lib/com.jdojo.intro-1.0.jar!/module-info.class
Compiled from "module-info.java"
module com.jdojo.intro {
  requires java.base;
}

第一個命令使用文件名,第二個命令使用jar 加URL。 兩個命令都使用相對路徑。 如果需要,可以使用絕對路徑。

輸出表明module-info.class文件都包含相同的模塊聲明。 需要使用-verbose選項(或-v選項)打印類信息以查看類屬性。 以下命令從mods目錄打印module-info.class文件信息。

C:\Java9Revealed>javap -verbose mods\com.jdojo.intro\module-info.class

以下是部分輸出,並顯示模塊版本和主類名稱並不存在。

Classfile /C:/Java9Revealed/mods/com.jdojo.intro/module-info.class
  Last modified Jan 22, 2017; size 161 bytes
...
Constant pool:
   #1 = Class              #8             // "module-info"
   #2 = Utf8               SourceFile
   #3 = Utf8               module-info.java
   #4 = Utf8               Module
   #5 = Module             #9             // "com.jdojo.intro"
   #6 = Module             #10            // "java.base"
   #7 = Utf8               9-ea
   #8 = Utf8               module-info
   #9 = Utf8               com.jdojo.intro
  #10 = Utf8               java.base
{
}
SourceFile: "module-info.java"
Module:
  #5,0                                  // "com.jdojo.intro"
  #0
  1                                     // requires
  #6,8000                               // "java.base" ACC_MANDATED
  #7                                    // 9-ea
  0                                     // exports
  0                                     // opens
  0                                     // uses
  0                                     // provides

以下命令從lib\com.jdojo.intro-1.0.jar文件中打印module-info.class文件信息,並顯示模塊版本和主類名稱確實存在。 顯示部分輸出。

C:\Java9Revealed>javap -verbose jar:file:lib/com.jdojo.intro-1.0.jar!/module-info.class
Classfile jar:file:lib/com.jdojo.intro-1.0.jar!/module-info.class
...
Constant pool:
  ...
   #6 = Utf8               com/jdojo/intro
   #7 = Package            #6             // com/jdojo/intro
   #8 = Utf8               ModuleMainClass
   #9 = Utf8               com/jdojo/intro/Welcome
  #10 = Class         #9      // com/jdojo/intro/Welcome
  ...
  #14 = Utf8               1.0
  ...
{
}
SourceFile: "module-info.java"
ModulePackages:
  #7                                      // com.jdojo.intro
ModuleMainClass: #10                      // com.jdojo.intro.Welcome
Module:
 #13,0                                    // "com.jdojo.intro"
 #14                                      // 1.0
 1                                        // requires
 #16,8000                                 // "java.base" ACC_MANDATED

還可以拆分模塊中的類的代碼。 需要指定模塊路徑,模塊名稱和類的完全限定名稱。 以下命令從其模塊化JAR中打印com.jdojo.intro.Welcome類的代碼:

C:\Java9Revealed>javap --module-path lib
 --module com.jdojo.intro com.jdojo.intro.Welcome
Compiled from "Welcome.java"
public class com.jdojo.intro.Welcome {
  public com.jdojo.intro.Welcome();
  public static void main(java.lang.String[]);
}

還可以打印系統類的類信息。 以下命令從java.base模塊中打印java.lang.Object類的類信息。 打印系統類信息時,不需要指定模塊路徑。

C:\Java9Revealed>javap --module java.base java.lang.Object
Compiled from "Object.java"
public class java.lang.Object {
  public java.lang.Object();
  public final native java.lang.Class<?> getClass();
  public native int hashCode();
  public boolean equals(java.lang.Object);
  ...
}

如何打印系統模塊(如java.base或java.sql)的模塊聲明? 系統模塊是以特殊文件格式打包的,而不是模塊化的JAR。 JDK 9引入了一個名為jrt的新URL方案(jrt是Java運行時的縮寫)來引用Java運行時映像(或系統模塊)的內容。
其語法如下:

jrt:/<module>/<path-to-a-file>

以下命令打印名為java.sql的系統模塊的模塊聲明:

C:\Java9Revealed>javap jrt:/java.sql/module-info.class
Compiled from "module-info.java"
module java.sql@9-ea {
  requires java.base;
  requires transitive java.logging;
  requires transitive java.xml;
  exports javax.transaction.xa;
  exports javax.sql;
  exports java.sql;
  uses java.sql.Driver;
}

以下命令打印java.se的模塊聲明,它是一個聚合模塊:

C:\Java9Revealed>javap jrt:/java.se/module-info.class
Compiled from "module-info.java"
module java.se@9-ea {
  requires transitive java.sql;
  requires transitive java.rmi;
  requires transitive java.desktop;
  requires transitive java.security.jgss;
  requires transitive java.security.sasl;
  requires transitive java.management;
  requires transitive java.logging;
  requires transitive java.xml;
  requires transitive java.scripting;
  requires transitive java.compiler;
  requires transitive java.naming;
  requires transitive java.instrument;
  requires transitive java.xml.crypto;
  requires transitive java.prefs;
  requires transitive java.sql.rowset;
  requires java.base;
  requires transitive java.datatransfer;
}

還可以使用jrt方案來引用系統類。 以下命令在java.base模塊中打印java.lang.Object類的類信息:

C:\Java9Revealed>javap jrt:/java.base/java/lang/Object.class
Compiled from "Object.java"
public class java.lang.Object {
  public java.lang.Object();
  public final native java.lang.Class<?> getClass();
  public native int hashCode();
  public boolean equals(java.lang.Object);
  ...
}

十三. 總結

如果模塊需要使用另一個模塊中包含的公共類型,則第二個模塊需要導出包含類型的包,而第一個模塊需要讀取第二個模塊。

一個模塊使用exports語句導出其包。 模塊可以將其軟件包導出到特定模塊。 導出包中的公共類型在編譯時和運行時可用於其他模塊。 導出的包不允許對公共類型的非公開成員深層反射。

如果一個模塊允許其他模塊訪問所有類型的成員(公共和非公共)使用反射,則該模塊必須被聲明為開放模塊,或者模塊可以使用打開語句選擇性地開放包。 從開放包中訪問類型的模塊不需要讀取包含這些打開包的模塊。

一個模塊使用require語句來聲明對另一個模塊的依賴。 這種依賴可以使用transitive修飾符聲明為傳遞依賴。 如果模塊M聲明對模塊N的傳遞依賴性,則聲明對模塊M依賴的任何模塊聲明對模塊N的隱含依賴。

依賴關系可以在編譯時聲明為必須的,但在運行時可以使用require語句中的static修飾符為可選依賴。 依賴關系在運行時可以同時是可選和傳遞的。

JDK 9中的模塊系統已經改變了公共類型(public)的含義。 在模塊中定義的公共類型可能屬於三個類別之一:僅在定義模塊中公開,僅限於特定模塊,或向所有人公開。

基於一個模塊的聲明以及它是否有一個名稱,有幾種類型的模塊。基於模塊是否具有名稱,模塊可以是命名模塊或未命名模塊。當模塊具有名稱時,可以在模塊聲明中明確指定名稱,或者可以自動(或隱式地)生成名稱。如果名稱在模塊聲明中明確指定,則稱為顯式模塊。如果名稱由模塊系統通過讀取模塊路徑上的JAR文件名生成,則稱為自動模塊。如果您在不使用open修飾符的情況下聲明模塊,則稱為正常模塊。如果使用打開的修飾符聲明模塊,則稱為開放模塊。基於這些定義,開放模塊也是顯式模塊和命名模塊。自動模塊是一個命名模塊,因為它具有自動生成的名稱,但它不是顯式模塊,因為它在模塊系統在編譯時和運行時被隱式聲明。

將JAR(而不是模塊JAR)放在模塊路徑上時,JAR表示一個自動模塊,其名稱是從JAR文件名派生的。 自動模塊讀取所有其他模塊,並將其所有軟件包導出並打開。

在JDK 9中,類加載器可以從模塊或類路徑加載類。 每個類加載器都維護一個名為未命名模塊的模塊,該模塊包含從類路徑加載的所有類型的模塊。 一個未命名的模塊讀取每個其他模塊。 它導出並打開所有其他模塊的所有包。 未命名的模塊沒有名稱,因此顯式模塊無法聲明對未命名模塊的編譯時依賴。 如果顯式模塊需要訪問未命名模塊中的類型,則前者可以使用自動模塊作為橋梁或使用反射。

可以使用javap工具打印模塊聲明或屬性。 使用工具的-verbose(或-v)選項打印模塊描述符的類屬性。 JDK 9以特殊格式存儲運行時映像。 JDK 9引入了一個名為jrt的新文件方案,可以使用它來訪問運行時映像的內容。 它的語法是jrt:/ <module> / <path-to-a-file>


免責聲明!

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



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