Java学习之旅第三季-28:模块系统(二)

上小结我介绍了模块声明文件中的两个关键字 requires 与 exports ,本小节继续介绍另外两个关键字 opens 与 open。

28.1 opens子句

opens 子句用于在模块声明内部定义可在运行时供所有模块进行深度反射(包括public及private类型)使用的包,这样包的公共类型和私有类型都可以通过其他模块中的代码使用深度反射进行访问。

opens <包名> to <模块名>

上述语法中的 to 后的模块名表示运行时可以使用反射访问前面包的模块。

需要注意的是,对于同一个包,可以同时使用 exports 和 opens 子句。在这种情况下,该包在编译时和运行时都可供访问,并且在运行时还能进行深度反射。其次,opens 子句不能用于 open 模块内部;也不能使用通配符,并且不能包含超过一个包。

所谓深度反射(Deep Reflection),即所有成员都可以在运行时通过反射进行访问,不仅仅是获取基本信息。

下面示例只针对模块1的module-info.java文件,在module2的module-info.java文件中只保留requires子句:

module com.laotan.module2 {
    requires com.laotan.module1;
}

示例1:模块1中没有exports与opens子句:

module com.laotan.module1 {
}

这种情况下,模块2中仅仅使用requires com.laotan.module1; 导致的后果就是编译时无法直接使用模块1中的任何成员,但是使用反射可以获取模块1中的成员的信息:

package com.laotan.module2;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Optional;

/**
 * @author 老谭
 */
public class M2Demo1 {
    static void main() throws Exception {
        // 方式1
        Class<?> c1 = Class.forName("com.laotan.module1.M1Demo1");
        for (Method method : c1.getDeclaredMethods()) {
            System.out.println(method.getName());
        }
        
        // 方式2:使用模块访问
        Optional<Module> optional = ModuleLayer.boot().findModule("com.laotan.module1");
        Class c2 = Class.forName(optional.get(), "com.laotan.module1.M1Demo1");
        Constructor constructor = c2.getDeclaredConstructor();
        System.out.println(constructor.getName());

        Method m1 = c2.getDeclaredMethod("m1");
        System.out.println(m1.getName());

        Method m2 = c2.getDeclaredMethod("m2");
        System.out.println(m2.getName());
    }
}

运行结果如下:

m2
m1
com.laotan.module1.M1Demo1
m1
m2

但是使用以下语句则会报错:

Object o = constructor.newInstance();

示例2:模块1中只有exports子句:

module com.laotan.module1 {
    exports com.laotan.module1 to com.laotan.module2;
}

在模块2中使用

package com.laotan.module2;

import com.laotan.module1.M1Demo1;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Optional;

/**
 * @author 老谭
 */
public class M2Demo1 {
    static void main() throws Exception {
        var demo1 = new M1Demo1();
        // 方式1
        Class<?> c1 = Class.forName("com.laotan.module1.M1Demo1");
        for (Method method : c1.getDeclaredMethods()) {
            System.out.println(method.getName());
        }
        // 方式2:使用模块访问
        Optional<Module> optional = ModuleLayer.boot().findModule("com.laotan.module1");
        Class c2 = Class.forName(optional.get(), "com.laotan.module1.M1Demo1");
        Constructor constructor = c2.getDeclaredConstructor();
        System.out.println(constructor.getName());
        Object o = constructor.newInstance();

        Method m1 = c2.getDeclaredMethod("m1");
        System.out.println(m1.getName());
        m1.invoke(o);

        Method m2 = c2.getDeclaredMethod("m2");
        System.out.println(m2.getName());
        m2.setAccessible(true);
        m2.invoke(o);
    }
}

这种情况下编译时可以访问模块1中的成员。但是运行时私有方法无法访问,当然私有属性也是无法访问的:

m2
m1
com.laotan.module1.M1Demo1
m1
public m1
m2
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make private void com.laotan.module1.M1Demo1.m2() accessible: module com.laotan.module1 does not "opens com.laotan.module1" to module com.laotan.module2
	at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:353)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:329)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:277)
	at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:182)
	at java.base/java.lang.reflect.Method.setAccessible(Method.java:176)
	at com.laotan.module2/com.laotan.module2.M2Demo1.main(M2Demo1.java:33)

示例3:模块1中只有opens子句:

module com.laotan.module1 {
    opens com.laotan.module1 to com.laotan.module2;
}

模块2中的代码如下:

package com.laotan.module2;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Optional;

/**
 * @author 老谭
 */
public class M2Demo1 {
    static void main() throws Exception {
        // 方式1
        Class<?> c1 = Class.forName("com.laotan.module1.M1Demo1");
        for (Method method : c1.getDeclaredMethods()) {
            System.out.println(method.getName());
        }
        // 方式2:使用模块访问
        Optional<Module> optional = ModuleLayer.boot().findModule("com.laotan.module1");
        Class c2 = Class.forName(optional.get(), "com.laotan.module1.M1Demo1");
        Constructor constructor = c2.getDeclaredConstructor();
        System.out.println(constructor.getName());
        Object o = constructor.newInstance();

        Method m1 = c2.getDeclaredMethod("m1");
        System.out.println(m1.getName());
        m1.invoke(o);

        Method m2 = c2.getDeclaredMethod("m2");
        System.out.println(m2.getName());
        m2.setAccessible(true);
        m2.invoke(o);
    }
}

运行上述代码,没有任何问题,结果如下:

m2
m1
com.laotan.module1.M1Demo1
m1
public m1
m2
private m2

至此,我们应该可以看到exports子句与opens子句的区别:

  • exports 子句可以将指定的包导出,配合requires子句,实现编译时访问其他模块的成员,可以使用反射可以访问到所有成员的基本信息,但只能访问public成员
  • exports 子句配合requires子句,可以使用反射可以访问到所有成员的基本信息,且能访问所有成员

28.2 模块类型

目前Java中主要有两种类型的模块:命名模块和未命名模块。命名模块又分为普通模块和自动模块。普通模块还被区分为基本模块和开放模块。下图展示了模块的分类。

image-20251129001657856

未命名模块

一个未命名(Unnamed)的模块,正如其名称所示,没有名称且未被声明。它包含了类路径中的所有 JAR 文件或模块化的 JAR 文件。所有这些 JAR 文件共同构成了这个未命名的模块。

Java 平台模块系统首先在模块路径上查找特定类型。模块路径的搜索会先于类路径进行。如果在模块路径上未找到该类型,则会在类路径上进行搜索。如果在类路径上找到了该类型,它将成为所谓的未命名模块的一部分。每个类加载器的未命名模块都是独一无二的。对于每个类加载器,只有一个单一的未命名模块。

命名模块

这些命名的模块包含了模块系统中的所有模块,但不包括未命名的模块。有两个关键的区别能够区分未命名模块与命名模块。首先,未命名模块位于类路径中,而命名模块则位于模块路径中。其次,未命名模块没有名称,而每个命名模块都有一个名称。命名模块可以是普通模块,也可以是自动模块。命名模块是通过在 module-info.java 模块描述符文件中使用名称来声明的。这是一个模块归类为命名模块必须满足的唯一条件。

普通模块

普通模块这一概念实际上并不存在。我们使用词来定义一个非自动的命名模块。普通模块与自动模块的主要区别在于:普通模块有一个名为 module-info.java 的模块描述文件,而自动模块则没有。此外,普通模块是由开发人员明确声明的,这会在模块的模块描述文件中声明模块的依赖关系。自动模块的模块描述文件并非由开发人员提供。普通模块是通过关键字 module 后跟模块名称来声明的。之前我们介绍的所有模块都是普通模块。普通模块默认不会导出其任何包。此外,其导出语句必须明确指定。导出语句在编译时以及运行时都会导出的包。普通模块包括基本模块和开放模块。

基本模块

我们将每一个非开放模块的已命名模块称为基本模块。不过基本模块这一称呼在 JDK 中并未正式存在。我们使用它来定义既非自动模块也非开放模块的已命名模块。一个基本模块与普通模块具有相同的特性,只是它不会被用于深度反射。

开放模块

在一个模块内部,即使使用深度反射,其他模块中的代码也无法在编译时访问该模块中的包。然而,许多第三方库和框架在运行时会利用反射来访问 JDK 的内部内容。因此,除非授予反射访问权限,否则所有这些框架在 JDK 9 中都无法正常工作。
只有在命名模块中的代码可以向类路径中的代码授予反射访问权限。默认情况下,命名模块中的代码不会向其他命名模块中的代码授予此权限。因此,如果第三方库或框架位于类路径中,它们在 JDK 中默认具有反射访问权限。如果它们位于模块路径中,则在 JDK 中没有反射访问权限。但要授予一个模块中的所有包的反射访问权限,该模块应被声明为开放的。

一个开放模块的定义方式是:在关键字 module 前加上 open,就可以声明开放模块。如:

open module com.laotan.module1 {
    exports com.laotan.module1 to com.laotan.module2;
}

开放模块会使模块内部的所有包(包括公共包和私有包)都可供深度反射。当然也可以仅对特定的包进行反射,此时就可以使用之前介绍的 opens 子句,不过这样的话就不能使用 open 声明开发模块。

开放模块存在的原因在于,它们能让框架查看模块的内部结构,而使用基本模块则无法实现这一点。像 Spring、JPA 和 Hibernate 这样的框架在运行时就需要进行反射访问。

自动模块

自动模块是在将 JAR 文件放置到模块路径后自动创建出来的模块。将自动模块与普通模块进行比较,会发现以下两个重要的区别:

  • 自动模块默认会包含系统中现有的所有模块,这些模块包括我们自己的所有模块、JDK 镜像中的所有模块以及所有其他自动模块
  • 自动模块默认会导出其所有的包

自动模块可以访问类路径中的类型,并且特别适用于第三方代码。

自动模块用于将现有的应用程序迁移到支持模块系统的Java版本。

28.3 小结

本小结介绍了Java模块系统中的opens子句功能。opens允许在运行时对指定模块进行深度反射访问,包括私有成员。文章通过三个示例展示了不同配置下的反射访问效果:1)无exports和opens时仅能获取基本信息;2)仅exports时编译时可访问但运行时私有成员不可访问;3)仅opens时可在运行时访问所有成员。opens与exports可同时使用,分别控制编译时和运行时的访问权限。另外也介绍了开放模使用 open 关键字声明。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值