文 by / 林本托
Tips 做一个终身学习的人。
在此章节中,主要学习以下内容:
假设你现在已经有两个模块,分别是:
其中,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 语句用于将包导出到所有其他模块或某些命名模块。 导出的包中的所有公共类型都可以在编译时和运行时访问。 在运行时,可以使用反射来访问公共类型的公共成员。 即使在这些成员上使用了
方法,公共类型的非公开成员也无法使用反射。 exports 语句的一般语法如下所示:
- setAccessible(true)
- 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 项目,然后选择 "属性"。具体如下所示:
最后点击 "确定" 按钮。
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 类。 这里,
方法返回一个无权访问的 Address 类型的对象。 模块系统只是对 com.jdojo.address 模块定义进行了封装。 如果模块想要明确或者隐式地使用 Address 类,它必须读取 com.jdojo.address 模块。 如何解决? 简单的答案是 com.jdojo.person.test 模块通过将其声明模块来读取 com.jdojo.address 模块。
- john.getAddress()
- // 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
假设你正在开发由多个模块组成的库或框架。 其中有一个模块中的包含 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。
经过几次的设计和实验迭代,模块系统设计人员想出了一个变通的解决方案, 目前的设计允许拥有强大的封装,深层反射访问,或者两者一直。 规则如下:
声明开放模块的语法如下:
- open module com.jdojo.model {
- // Module statements go here
- }
在这里,com.jdojo.model 模块是一个开放模块。 其他模块可以在本模块中的所有软件包上对所有类型使用深层反射。 可以在开放模块中声明 exports, requires, uses, 和 provides 语句。 但不能在打开的模块中再声明 opens 语句。 opens 语句用于打开特定的包以进行深层反射。 因为开放模块打开所有的软件包进行深层反射,所以在开放模块中不允许再使用 open 语句。
打开一个包意味着允许其他模块对该包中的类型使用深层反射。 可以打开一个包指定给所有其他模块或特定的模块列表。 打开一个包到所有其他模块的打开语句的语法如下:
- 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 模块可以访问所有类型及其成员在运行时进行深度反射。
在本节中,解释如何打开模块和软件包进行深度反射。 从一个基本的用例开始,然后构建一个例子。 在这个例子中:
在 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()
方法来加载 com.jdojo.reflect.Item 类,并尝试打印该类的所有四个静态字段的值。 我们来运行 ReflectTest 类。它会生成以下错误:
- Class.forName()
- 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 模块包含在模块图中? 使用
命令行 VM 选项将此模块添加到默认的根模块中。 此选项的值是以逗号分隔的要添加到默认的根模块集的模块列表:
- --add-modules
- --add - modules < module1 > ,
- <module2 > ...
下图显示了 NetBeans 中 com.jdojo.reflect.test 项目的 "属性" 对话框,其中使用 VM 选项可将 com.jdojo.reflect 模块添加到默认的根模块集中。
将 com.jdojo.reflect 模块添加到默认的根模块后,运行时的模块图如下所示。
解决 com.jdojo.reflect 模块的另一种方法是添加一个
声明。 这样,com.jdojo.reflect 模块将被解析为 com.jdojo.reflect.test 模块的依赖项。 如果使用此选项,则不需要使用 --
- require com.jdojo.reflect;
选项。
- 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 类已成功加载。当程序试图在字段上调用
时,会为每个字段抛出一个 InaccessibleObjectException 异常。注意输出中四个错误消息的区别。对于 s,t 和 u 字段,错误消息表示无法访问它们,因为 com.jdojo.reflect 模块未打开 com.jdojo.reflect 包。对于 v 字段,错误消息指出该模块不导出 com.jdojo.reflect 包。不同错误消息背后的原因是 v 字段是公开的,而其他字段是非公开的。要访问公共字段,需要导出包,这是允许的最小可访问性。要访问非公共字段,必须打开该包,这是允许的最大可访问性。
- setAccessible(true)
下面包含 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;
- }
这个例子基本结束了! 有几点值得注意:
我们来看看这些模块的最终版本。 下面两个包含这些模块声明的修改版本。
- // 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 类时,不需要使用
VM 选项。 com.jdojo.reflect 模块将被解析,因为在 com.jdojo.reflect.test 模块的模块声明中添加了
- --add-modules
语句。 下图显示了运行 ReflectTest 类时创建的模块图。
- requires com.jdojo.reflect;
在 JDK 9 之前,有四种访问类型:
在 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 包。 必须将此类的包更改为任何可观察模块中不存在的名称。
声明模块有几个约束。 如果违反了这些规定,将在编译时或启动时收到错误:
Java 已经存在了 20 多年,旧的和新的应用程序将继续使用未被模块化或永远不会被模块化的库。 如果 JDK 9 迫使所有人将其应用程序模块化,JDK 9 可能不会被广泛采用。 JDK 9 设计师保持向后兼容性。 可以通过以自己的速度调整应用程序或通过决定不通过在 JDK 9 中运行现有应用程序来模块化来采用 JDK 9。在大多数情况下,在 JDK 8 或更早版本中工作的应用程序将继续工作 JDK 9 没有任何变化。 为了简化迁移,JDK 9 定义了四种类型的模块:
实际上,将会遇到六个不同类型的模块的术语,对于 JDK 9 的初学者来说,此处最为模糊。 其他两种类型的模块用于传达这四种类型的模块的更广泛的类别。 下图显示了所有模块类型的图示。
在描述模块的主要类型之前,先简要介绍上图表示的模块类型。
基于这些定义,开放模块也是显式模块和命名模块。 自动模块是一个命名模块,因为它具有自动生成的名称,但它不是显式模块,因为它在模块系统在编译时和运行时被隐式声明。 以下小节介绍这些模块类型。
使用模块声明明确声明而不使用 opem 修饰符的模块始终被赋予一个名称,它被称为普通模块或简化模块。 到目前为止,你一直在使用大多数普通的模块。 目前一直将普通模块称为模块,后面继续在这个意义上使用这个术语,除非需要区分四种类型的模块。 默认情况下,普通模块中的所有类型都被封装。 普通模块的一个例子如下: module a.normal.module {// Module statements go here}
如果模块声明包含 open 修饰符,则该模块被称为开发模块。 开放模块的一个例子如下:
- open module a.open.module {
- // Module statements go here
- }
为了向后兼容,查找类型的类路径机制仍然可以在 JDK 9 中使用。可以选择将 JAR 放在类路径,模块路径和两者的组合上。 请注意,可以在模块路径和类路径上放置模块化 JAR 以及 JAR。
将 JAR 放在模块路径上时,JAR 被视为一个模块,称为自动模块。 名称自动模块是从模块从 JAR 自动定义的事实得出的,不通过添加 module-info.class 文件来显式声明模块。 自动模块有一个名称。 自动模块的名称是什么? 它读取哪些模块以及导出哪些软件包?
自动模块其实也是一个有名字的模块。 其名称和版本由 JAR 文件的名称派生,对应以下规则:
按顺序应用这些规则可以提供模块名称和模块版本。 在本节结尾处,展示如何使用 JAR 文件确定自动模块的名称。 下面列出了几个 JAR 名称,以及派生的自动模块名称和版本。 请注意,该表不显示 JAR 文件名中的扩展名. jar。
Jar 名词 | 模块名称 |
---|---|
com.jdojo.intro-1.0 | com.jdojo.intro |
junit-4.10 | junit |
jdojo-logging1.5.0 | 有错误 |
spring-core-4.0.1.RELEASE | spring.core |
jdojo-trans-api_1.5_spec-1.0.0 | 有错误 |
_ | 有错误 |
我们来看看表中的三个奇怪的情况,如果你将 JAR 放在模块路径中,你会收到一个错误。 生成错误的第一个 JAR 名称是 jdojo-logging1.5.0。 让我们应用规则来导出此 JAR 的自动模块名称:
生成错误的另一个 JAR 名称是 jdojo-trans-api_1.5_spec-1.0.0。 我们来应用规则来推导出这个 JAR 的自动模块名称:
表中的最后一个条目包含一个下划线()作为 JAR 名称。 也就是说,JAR 文件被命名为. jar。 如果应用规则,下划线将被一个点替换,并且该点将被删除,因为它是名称中唯一的字符。 最后一个空字符串,这不是一个有效的模块名称。 如果无法从其名称导出有效的自动模块名称,则放置在模块路径上的 JAR 将抛出异常。 例如,模块路径上的_.jar 文件将导致以下异常:
- java.lang.module.ResolutionException: Unable to derive module descriptor
- for: _.jar
可以使用带有
选项的 jar 命令打印模块化 JAR 的模块描述符,并打印 JAR 派生的自动模块名称的名称。 对于 JAR,它还打印 JAR 包含的包的列表。 使用命令的一般语法如下:
- --describe-module
- 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 必须通过使用
命令行选项作为根模块添加到模块图中并将 Q 指定为此选项的值。
- --add-modules
以下命令描述了 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
可以将 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
- }
未命名的模块读取其他模块,导出和打开它自己的包给其他模块,使用以下规则:
Tips 未命名模块可能包含一个包,此包被一个命名模块导出。 在这种情况下,未命名模块中的包将被忽略。
我们来看看使用未命名模块的两个例子。 在第一个例子中,普通模块将使用反射访问未命名的模块。普通模块在编译时无法访问未命名的模块。 在第二个例子中,一个未命名的模块访问一个普通模块。
不必声明一个未命名的模块。 若果要有一个未命名的模块,需要在类路径上放置一个 JAR 或一个模块化 JAR。 通过将其模块化 JAR 放置在类路径上,将 com.jdojo.reflect 模块重用为未命名模块。
下列两段代码分别包含名为 com.jdojo.unnamed.test 的模块和模块中的 Main 类的模块声明。 在
方法中,该类尝试加载 com.jdojo.reflect.Item 类并读取其字段。 为了保持代码简单,直接在
- main()
方法添加了一个 throws 子句。
- main
- // 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 项目的类路径中,如下图所示。
要运行 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 中添加此选项。
再次运行,打印如下信息:
- Item.v = 40
输出显示,未命名的模块能够从命名模块的导出包访问公共类型及其公共成员。 注意,无法访问 com.jdojo.unnamed.Main 类中的 Item 类的其他静态变量(s,t 和 u),因为它们不是 public 的。
当将应用程序迁移到 JDK 9 时,应该考虑到模块系统提供的两个好处:强大的封装和可靠的配置。 你的目标是应用程序由普通模块组成,除了几个开放模块。 牢记应用程序的种类,其他代码的相互依赖关系,以及不同的配置需求。下面是一些通用的准则,可以帮助你完成迁移过程。
在 JDK 9 之前,一个有意义的 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
来源: http://www.cnblogs.com/IcanFixIt/p/6994501.html