使用 requires transitive
隐式可读性
模块系统对访问其他模块中的代码有严格的规则,其中之一是访问模块必须读取被访问的模块。建立可读性的最常见方法是让一个模块需要另一个模块,但这并不是唯一的方法。如果一个模块在其自己的 API 中使用来自另一个模块的类型,那么使用第一个模块的任何外部人员都将被迫也需要第二个模块。除非第一个模块对第二个模块使用 requires transitive
,这将为任何读取第一个模块的模块隐式地提供第二个模块的可读性。这有点令人困惑,但你将在几分钟内明白。
注意: 你需要了解 模块系统基础知识 才能充分利用本文。
隐式可读性
在常见情况下,模块在内部使用依赖项,而外部世界对此一无所知。例如,java.prefs
,它 requires
java.xml
: 它需要 XML 解析功能,但它自己的 API 既不接受也不返回来自 java.xml 包的类型。
但是,还有另一种用例,其中依赖项并非完全是内部的,而是存在于模块之间的边界。在这种情况下,一个模块依赖于另一个模块,并在其自己的公共 API 中公开来自被依赖模块的类型。一个很好的例子是 java.sql
。它也使用 java.xml,但与 java.prefs 不同,它不仅仅是在内部使用 - 公共类 java.sql.SQLXML
映射 SQL XML 类型,因此在其自己的 API 中使用来自 java.xml 的类型。类似地,java.sql 的 Driver
有一个方法 getParentLogger()
,它返回一个 Logger
,它是来自 java.logging 模块的类型。
在这种情况下,想要调用模块(例如 java.sql)的代码可能需要使用来自被依赖模块(例如 java.xml、java.logging)的类型。但如果它没有也读取被依赖模块,它就无法做到这一点。因此,为了使模块可用,所有客户端都必须显式地依赖于第二个模块。识别和手动解决这些隐藏的依赖项将是一项繁琐且容易出错的任务。
这就是隐式可读性发挥作用的地方。它扩展了模块声明,以便一个模块可以向任何依赖于它的模块授予其依赖的模块的可读性。这种隐式可读性通过在 requires 子句中包含 transitive
修饰符来表示。
这就是为什么 java.sql 的模块声明如下所示
module java.sql {
requires transitive java.logging;
requires transitive java.transaction.xa;
requires transitive java.xml;
exports java.sql;
exports javax.sql;
uses java.sql.Driver;
}
这意味着任何读取 java.sql 的模块(通常是通过需要它)也将自动读取 java.logging、java.transaction.xa 和 java.xml。
何时依赖隐式可读性
模块系统的原始解释者包含一个关于何时使用隐式可读性的明确建议
一般来说,如果一个模块导出一个包含类型的包,其签名引用了第二个模块中的包,那么第一个模块的声明应该包含对第二个模块的
requires transitive
依赖关系。这将确保依赖于第一个模块的其他模块将自动能够读取第二个模块,从而访问该模块导出包中的所有类型。
但是你应该将此扩展到什么程度?回顾 java.sql 的例子,使用它的模块是否也应该需要 java.logging?从技术上讲,这种声明是不需要的,并且可能显得多余。
要回答这个问题,我们必须看看虚拟模块究竟如何使用 java.logging。它可能只需要读取它,以便你能够调用 Driver.getParentLogger()
,例如更改记录器的日志级别,仅此而已。在这种情况下,你的代码与 java.logging 的交互发生在你与 java.sql 中的 Driver
的交互的直接附近。上面我们称之为两个模块之间的边界。
或者,你的模块实际上可能在整个代码中使用日志记录。然后,来自 java.logging 的类型出现在许多独立于 Driver
的地方,并且不能再被认为仅限于你的模块和 java.sql 的边界。
建议仅在模块(例如 java.logging)的类型仅在边界上用于 requires transitive
它(例如 java.sql)的模块时,才依赖于它的隐式可读性。否则,即使不是严格需要,也应该显式地需要它。这种方法阐明了系统的结构,并且也为各种重构提供了模块声明的未来保障。
上次更新: 2021 年 9 月 14 日