自从20年前,Java允许使用反射机制访问所有成员,包括私有,公共,包和受保护的类。 你可以访问类或对象的私有成员,需要做的是在成员(Field,Method等)上调用 setAccessible(true) 方法。下面章节讨论访问非公开类型的反射方法: 深度反射(deep reflection) 。
当导出一个模块中的包时,其他模块只能在编译时静态(statically)访问导出包中的公共类和公共类中的公共/受保护的成员,或者在运行时使用反射。这就是模块系统设计时的大问题, 有几个著名的框架,如Spring和Hibernate,它们很大程度上依赖于对程序库中定义的类成员使用深度反射进行访问。
模块系统的设计人员在设计模块化代码的深层反射访问方面面临着巨大的挑战。 允许对导出的包中的类使用深度反射违反了模块系统的强封装的主旨。即使模块开发人员不想公开模块的某些部分,他们也可以被外部代码访问。
另一方面,通过不允许深度反射将会消除Java社区中一些广泛使用的框架,并且还会破坏依赖于深层反射的许多现有应用程序。 由于这个限制,许多现有应用程序将不会迁移到Java 9。
经过几次的设计和实验迭代,模块系统设计人员想出了一个变通的解决方案, 目前的设计允许拥有强大的封装,深度反射访问,或者两者同时具备。 规则如下:
-
导出的包将允许在其他模块在编译时和运行时访问公共类型及其公共成员。 如果不导出包,则该包中的所有类型都不可被其他模块访问。 这提供了强大的封装。
-
可以打开一个模块,以便在运行时对该模块中的所有包中的所有类进行深度反射。 这样的模块称为 开放模块 。
-
你可以有一个普通的模块 —— 一个不能进行深度反射的模块(在运行时打开深度反射的特定 软件包 )。 所有其他软件包(非开放软件包)都被强封装。 模块中允许深度反射的软件包称为 开放软件包 。
-
有时,可能希望在编译时访问包中的类,以便根据该包中的类编写代码,同时,您也可以在运行时深度反射访问这些类型。 可以导出并打开相同的包来实现此目的。
开放模块
声明开放模块的语法如下:
open module com.jdojo.model { // Module statements go here}
在这里,com.jdojo.model模块是一个开放模块。 其他模块可以在本模块中的所有软件包上对所有类型使用深度反射。
可以在开放模块中声明 exports , requires , uses, 和 provides 语句。 但不能在打开的模块中再声明 open s 语句。
opens 语句用于打开特定的包以进行深度反射。 因为开放模块打开所有的软件包进行深度反射,所以在开放模块中不允许再使用 opens 语句。
开放包
开放一个包意味着允许其他模块对该包中的类使用深度反射。 可以 open 一个包指定给所有其他模块或特定的模块列表。
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的类。下面包含了模块和类的源代码。
// Item.javapackage 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类。它会生成以下错误:
该错误消息表示当尝试加载com.jdojo.reflect.Item类时抛出ClassNotFoundException异常。 这个错误源于另一个问题。
当尝试加载类时,包含该类的模块必须为模块系统所知。 如果在JDK 9之前收到ClassNotFoundException,则表示该类不在类路径中。 可以将包含该类的目录或JAR添加到类路径中,该错误将被解决。在Java 9中,使用模块路径找到模块。 所以,我们在模块路径上添加com.jdojo.reflect模块,然后运行ReflectTest类。
在NetBeans中,您需要使用属性对话框将com.jdojo.reflect项目添加到com.jdojo.reflect.test模块的模块路径中,如下图所示:
也可以使用以下命令运行ReflectTest类,假设已在NetBeans中构建了这两个项目,并且项目的dist目录包含模块化JAR。
C:Java9Revealed>java --module-path com.jdojo.reflectdist;com.jdojo.reflect.testdist --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选项将此模块添加到默认的根模块中。 此选项的值是以逗号分隔的要添加到默认的根模块集的模块列表:
在NetBeans中重新运行ReflectTest类。 还可以使用以下命令来运行它:
C:Java9Revealed>java --module-path com.jdojo.reflectdist;com.jdojo.reflect.testdist --add-modules com.jdojo.reflect--module com.jdojo.reflect.test/com.jdojo.reflect.test.ReflectTest
会得到以下错误信息:
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.javamodule com.jdojo.reflect { exports com.jdojo.reflect; }
重新运行ReflectTest类,会得到以下错误信息:
如预期的那样,可以访问公共的v域的值。 导出包允许仅访问公共类型及其公共成员。 不能访问其他非公开字段。
要获得对Item类的深层反射访问,解决方案是打开整个模块或包含Item类的包。
下面包含com.jdojo.reflect模块的修改版本,它将其声明为一个开放模块。 一个开放的模块在运行时导出所有的软件包,用于深度反射。
// module-info.javaopen 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.javamodule com.jdojo.reflect { opens com.jdojo.reflect; }
这个例子基本结束了! 有几点值得注意:
-
开放模块或具有开放包的模块允许访问所有类型的成员以深度反射到其他模块,其他模块不必声明对第一个模块的依赖。
在此示例中,com.jdojo.reflect.test模块能够访问Item类及其成员,而不声明对com.jdojo.reflect模块的依赖。
这个规则是为了确保使用深层反射的Hibernate和Spring等框架不必声明对应用程序模块的访问依赖。 -
如果要在编译时访问包的公共API,并希望在运行时使用深度反射访问相同的包,则可以打开并导出相同的包。 在这个例子中,我们可以在com.jdojo.reflect模块中导出并打开com.jdojo.reflect包。
-
如果一个模块是开放的或者开放的包,你仍然可以声明对它们的依赖,但是没有必要。 此规则有助于迁移到Java 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类时创建的模块图。