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>

posted @ 2017-06-12 18:56  林本托  阅读(7711)  评论(2编辑  收藏  举报