限定的 exports
和 opens
模块系统允许模块导出和打开包以使其可供外部代码访问,在这种情况下,每个读取导出/打开模块的模块都可以访问这些包中的类型。这意味着我们必须在强封装包或始终使其可供所有人访问之间做出选择。为了处理不适合这种二分法的用例,模块系统提供了 exports
和 opens
指令的限定变体,这些变体只允许特定模块访问。
注意:您需要了解 模块系统基础知识 和 如何打开包 才能充分利用本文。
包的限定导出/打开
exports
指令可以通过在后面加上 to $MODULES
来限定,其中 $MODULES
是目标模块名称的逗号分隔列表。对于 exports to
指令中命名的模块,该包将与常规 exports
指令一样可访问。对于所有其他模块,该包将与没有 exports
一样被强封装。opens
指令也是如此,它也可以用 to $MODULES
限定,效果相同:对于目标模块,该包是打开的;对于所有其他模块,它是强封装的。
JDK 本身有很多限定导出的示例,但我们将重点关注 java.xml,它定义了 Java API for XML Processing (JAXP)。它的六个内部包,以 com.sun.org.apache.xml.internal
和 com.sun.org.apache.xpath.internal
为前缀,被 java.xml.crypto(XML 加密 API)使用,因此被导出到它(并且仅导出到它)
module java.xml {
// lots of regular exports
exports com.sun.org.apache.xml.internal.dtm to
java.xml.crypto;
exports com.sun.org.apache.xml.internal.utils to
java.xml.crypto;
exports com.sun.org.apache.xpath.internal to
java.xml.crypto;
exports com.sun.org.apache.xpath.internal.compiler to
java.xml.crypto;
exports com.sun.org.apache.xpath.internal.functions to
java.xml.crypto;
exports com.sun.org.apache.xpath.internal.objects to
java.xml.crypto;
exports com.sun.org.apache.xpath.internal.res to
java.xml.crypto;
// lots of services usages
}
关于编译的两个小说明
- 如果编译声明了限定导出/打开的模块,并且找不到目标模块,编译器将发出警告。这不是错误,因为目标模块被提及但不是必需的。
- 不允许在
exports
和exports to
中使用包或在opens
和opens to
指令中使用包。如果存在任何一对指令,限定变体将实际上毫无用处,因此这种情况被解释为实现错误,因此会导致编译错误。
还有两个细节需要指出
- 目标模块可以依赖于导出/打开模块(实际上 java.xml.crypto 依赖于 java.xml),从而创建一个循环。仔细想想,除非使用 隐式可读性,否则实际上必须是这样 - 否则目标模块如何读取导出/打开模块?
- 每当新的模块需要访问限定导出的包时,都需要更改拥有模块,以便它允许访问此新模块。虽然让导出模块控制谁可以访问包是限定导出的全部意义,但它仍然很麻烦。
何时使用限定导出
如前所述,限定导出的用例是控制哪些模块可以访问相关包。这种情况多久发生一次?一般来说,每当一组模块想要在它们之间共享功能而不公开它时。
这与在引入模块系统之前隐藏实用程序类的难题是对称的。一旦实用程序类必须跨包可用,它就必须是公共的,但在 Java 9 之前,这意味着所有其他代码都可以访问它。强封装通过允许我们使公共类在模块之外不可访问来解决了这个问题。
现在我们遇到了类似的情况,我们想要隐藏一个包(以前是一个类),但一旦它必须跨模块(包)可用,它就必须被导出(公开),因此所有其他模块(所有其他类)都可以访问它。这就是限定导出发挥作用的地方。它们允许模块在它们之间共享一个包,而不使其普遍可用。这使得它非常适合由多个模块组成的库和框架,这些库和框架想要共享代码,而无需客户端使用它。它也将在大型应用程序中派上用场,这些应用程序想要限制对特定 API 的依赖关系。
限定导出可以被视为将强封装从保护工件中的类型提升到保护模块集中的包。
何时使用限定打开
限定导出具有您控制的目标模块,这使得这些指令成为防止同事和用户在内部 API 上引入意外依赖关系的重要工具。另一方面,限定打开的目标模块通常是框架,无论您是将包打开以供所有模块反射还是仅打开以供 Hibernate 反射,无论哪种方式,Spring 都不会开始依赖它。因此,限定打开的用例远小于限定导出。
限定打开的一个缺点是,直到框架开始采用基于 Lookup
/VarHandle
的方法(允许“转发”反射访问)之前,包必须始终打开以供执行实际反射的特定模块访问。因此,在规范和实现分离的情况下(例如,JPA 和 Hibernate),您可能会发现自己必须将实体包打开以供实现访问,而不是 API(例如,Hibernate 模块而不是 JPA 模块)。如果您的项目试图坚持标准并避免在代码中提及任何实现,那将是不幸的。
总而言之,打开包以供反射访问的良好默认方法是不限定访问,除非您的项目对自己的代码使用大量反射,在这种情况下,其好处类似于限定导出。仅打开以供框架访问似乎不值得麻烦,并且在需要针对特定实现模块的情况下,应该完全避免。
上次更新: 2021 年 9 月 14 日