JEP 261:模块系统
概括
实现JSR 376指定的 Java 平台模块系统,以及相关的 JDK 特定更改和增强。
描述
Java 平台模块系统(JSR 376)指定了对 Java 编程语言、Java 虚拟机和标准 Java API 的更改和扩展。此 JEP 实现了该规范。因此,javac
编译器、HotSpot 虚拟机和运行时库将模块实现为一种基本的新型 Java 程序组件,并在开发的各个阶段提供可靠的配置和强大的模块封装。
此 JEP 还更改、扩展和添加了 JDK 特定的工具和 API,这些工具和 API 超出了 JSR 的范围,与编译、链接和执行相关。对其他工具和 API (例如,工具和 Doclet API)的相关更改javadoc
是单独 JEP 的主题。
此 JEP 假设读者熟悉最新的模块系统状态文档以及其他Project Jigsaw JEP:
阶段
javac
对于熟悉的编译时(命令)和运行时(java
运行时启动器)阶段,我们添加了_链接时_的概念,这是两者之间的一个可选阶段,其中可以将一组模块组装并优化为自定义运行-时间图像。链接工具是JEP 282jlink
的主题;许多新的命令行选项由 实现,并且也由.javac``java``jlink
模块路径
javac
、jlink
、 和命令java
以及其他几个命令现在接受指定各种模块路径的选项。模块路径是一个序列,其中的每个元素要么是_模块定义_,要么是包含模块定义的目录。每个模块定义是
-
模块_工件_,_即_包含已编译模块定义的模块化 JAR 文件或 JMOD 文件,或者
-
一个_分解的模块目录_,按照惯例,其名称是模块的名称,其内容是与包层次结构相对应的“分解”目录树。
在后一种情况下,目录树可以是已编译的模块定义,填充有单独的类和资源文件以及module-info.class
根处的文件,或者在编译时,目录树可以是源模块定义,填充有单独的源文件和module-info.java
根处的文件。
与其他类型的路径一样,模块路径由由主机平台的路径分隔符分隔的路径名字符串指定(':'
在大多数平台上,';'
在 Windows 上)。
模块路径与类路径有很大不同:类路径是定位单个类型和资源定义的方法,而模块路径是定位整个模块定义的方法。类路径的每个元素都是类型和资源定义的容器,即,JAR 文件或分解的包分层目录树。相比之下,模块路径的每个元素都是模块定义或目录,目录中的每个元素都是模块定义,_即_类型和资源定义的容器,_即_模块化 JAR 文件、JMOD 文件、或分解的模块目录。
在解析过程中,模块系统通过根据阶段沿几个不同的路径搜索以及通过搜索内置于环境中的已编译模块来定位模块,按以下顺序:
-
编译模块_路径_(由命令行选项指定
--module-source-path
)包含源形式的模块定义(仅限编译时)。 -
升级_模块路径_(
--upgrade-module-path
) 包含旨在优先于系统模块中或应用程序模块路径(编译时和运行时)上存在的任何可升级模块的编译定义使用的模块的编译定义。 -
系统_模块_是内置于环境(编译时和运行时)中的已编译模块。这些通常包括Java SE 和 JDK 模块,但在自定义链接映像的情况下,还可以包括库和应用程序模块。在编译时,可以通过选项覆盖系统模块
--system
,该选项指定从中加载系统模块的 JDK 映像。 -
应用程序模块路径(或
--module-path
简称-p
)包含库和应用程序模块(所有阶段 )的编译定义。在链接时,此路径还可以包含 Java SE 和 JDK 模块。
这些路径上存在的模块定义与系统模块一起定义了_可观察模块_的范围。
当在模块路径中搜索特定名称的模块时,模块系统会采用该名称的模块的第一个定义。版本字符串(如果存在)将被忽略;如果模块路径的元素包含多个同名模块的定义,则解析失败,编译器、链接器或虚拟机将报告错误并退出。构建工具和容器应用程序有责任配置模块路径,以避免版本冲突;模块系统的目标不是解决版本选择问题。
根模块
模块系统通过解决一组_根模块_相对于一组可观察模块的依赖关系的传递闭包来构造模块图。
当编译器编译未命名模块中的代码,或者java
调用启动器并将应用程序的主类从类路径加载到应用程序类加载器的未命名模块时,则_未命名模块的默认根模块集_为计算如下:
-
该
java.se
模块是根(如果存在)。如果它不存在,则java.*
升级模块路径上或导出至少一个包的系统模块中的每个模块(无条件限制)都是根。 -
升级模块路径上或系统模块中导出至少一个包的每个非
java.*
模块(无条件限制)也是根。
否则,默认的根模块集取决于阶段:
-
在编译时,它通常是正在编译的模块集(更多内容见下文);
-
在链接时它是空的;和
-
--module
在运行时,它是应用程序的主模块,通过( 或-m
简称)启动器选项指定。
有时需要将模块添加到默认根集中,以确保特定平台、库或服务提供商模块将出现在生成的模块图中。在任何阶段都可以选择
--add-modules <module>(,<module>)*
其中<module>
是模块名称,将指示的模块添加到默认的根模块集中。该选项可以多次使用。
作为运行时的特殊情况,如果<module>
是ALL-DEFAULT
,则将如上所述的未命名模块的默认根模块集添加到根集中。当应用程序是托管其他应用程序的容器时,这非常有用,而其他应用程序又可以依赖于容器本身不需要的模块。
作为运行时的另一种特殊情况,如果<module>
是,ALL-SYSTEM
则所有系统模块都将添加到根集中,无论它们是否在默认集中。测试工具有时需要这样做。该选项将导致许多模块被解析;一般来 说,ALL-DEFAULT
应该是首选。
作为最后一个特殊情况,在运行时和链接时,如果<module>
是ALL-MODULE-PATH
,则在相关模块路径上找到的所有可观察模块都将添加到根集中。 ALL-MODULE-PATH
在编译时和运行时都有效。这是供 Maven 等构建工具使用的,这些工具已经确保需要模块路径上的所有模块。这也是将自动模块添加到根集中的便捷方法。
限制可观察的模块
有时,限制可观察的模块是有用的,例如,为了调试,或者当主模块是由应用程序类加载器为类路径定义的未命名模块时,减少解析的模块数量。--limit-modules
可以在任何阶段使用该选项来执行此操作。其语法为:
--limit-modules <module>(,<module>)*
其中<module>
是模块名称。此选项的作用是将可观察模块限制为命名模块的传递闭包中的模块加上主模块(如果有)以及通过该--add-modules
选项指定的任何其他模块。
(为选项解释计算的传递闭包--limit-modules
是一个临时结果,仅用于计算有限的可观察模块集。将再次调用解析器以计算实际的模块图。)
提高可读性
在测试和调试时,有时需要安排一个模块读取其他模块,即使第一个模块不通过requires
其模块声明中的子句依赖于第二个模块。例如,这可能需要使被测模块能够访问测试工具本身,或者访问与工具相关的库。--add-reads
可以在编译时和运行时使用该选项来执行此操作。其语法为:
--add-reads <source-module>=<target-module>
其中<source-module>
和<target-module>
是模块名称。
该--add-reads
选项可以多次使用。每个实例的作用是添加从源模块到目标模块的可读性优势。本质上,这是模块声明中子句的命令行形式,或者是方法requires
的无限制形式的调用。因此,如果目标模块的包是通过源模块声明中的子句、方法的调用或选项的实例(定义如下)。此外,如果该模块被声明为打开,或者该包是通过源模块声明中的子句打开的,则此类代码将能够在运行时访问目标模块的包中的类型,或选项的实例(也在下面定义)。Module::addReads
exports``Module::addExports
--add-exports``opens``Module::addOpens
--add-opens
例如,如果测试工具将白盒测试类注入到java.management
模块中,并且该类扩展了(假设的)模块中导出的实用程序类testng
,则可以通过选项授予它所需的访问权限
--add-reads java.management=testng
作为一种特殊情况,如果是,<target-module>
则ALL-UNNAMED
可读性边缘将从源模块添加到所有当前和未来的未命名模块,包括与类路径相对应的模块。这允许模块中的代码由本身尚未转换为模块化形式的测试框架进行测试。
打破封装
有时需要违反模块系统定义的、编译器和虚拟机强制执行的访问控制边界,以便允许一个模块访问另一模块的某些未导出类型。这可能是理想的,以便例如_启用_内部类型的白盒测试,或者将不支持的内部API暴露给依赖于它们的代码。--add-exports
可以在编译时和运行时使用该选项来执行此操作。其语法为:
--add-exports <source-module>/<package>=<target-module>(,<target-module>)*
其中<source-module>
和<target-module>
是模块名称,<package>
是包的名称。
该--add-exports
选项可以多次使用,但对于源模块和包名称的任何特定组合最多使用一次。每个实例的作用是将指定包从源模块的限定导出添加到目标模块。本质上,这是模块声明中子句的命令行形式,或者是方法exports
的无限制形式的调用。因此,如果目标模块通过其模块声明中的子句、方法的调用或方法读取源模块,则目标模块中的代码将能够访问源模块的命名包中的公共类型。选项的实例。Module::addExports
requires``Module::addReads``--add-reads
例如,如果模块包含针对模块未导出包的jmx.wbtest
白盒测试,则可以通过选项授予其所需的访问权限com.sun.jmx.remote.internal``java.management
--add-exports java.management/com.sun.jmx.remote.internal=jmx.wbtest
作为一种特殊情况,如果是,<target-module>
那么ALL-UNNAMED
源包将被导出到所有未命名的模块,无论它们最初存在还是后来创建。因此,可以通过选项向类路径上的所有代码授予对模块sun.management
包的访问权限java.management
--add-exports java.management/sun.management=ALL-UNNAMED
该--add-exports
选项允许访问指定包的公共类型。有时需要更进一步,通过核心反射 API的setAccessible
方法启用对所有非公共元素的访问。可以在运行时使用该选项来执行此操作。它与选项具有相同的语法:--add-opens``--add-exports
--add-opens <source-module>/<package>=<target-module>(,<target-module>)*
其中<source-module>
和<target-module>
是模块名称,<package>
是包的名称。
该--add-opens
选项可以多次使用,但对于源模块和包名称的任何特定组合最多使用一次。每个实例的作用是将指定包的限定打开从源模块添加到目标模块。本质上,这是模块声明中子句的命令行形式,或者是方法opens
的无限制形式的调用。因此,只要目标模块读取源模块,目标模块中的代码就能够使用核心反射 API 来访问源模块的命名包中的所有类型(公共类型和其他类型)。Module::addOpens
在编译时,打开的包与未导出的包无法区分,因此--add-opens
在该阶段可能不会使用该选项。
和
--add-exports
选项--add-opens
必须非常小心地使用。您可以使用它们来访问库模块的内部 API,甚至是 JDK 本身,但您需要自行承担风险:如果该内部 API 被更改或删除,那么您的库或应用程序将失败。
修补模块内容
在测试和调试 时,有时用备用或实验版本替换选定的类文件或特定模块的资源,或者提供全新的类文件、资源甚至包是有用的。这可以通过--patch-module
编译时和运行时的选项来完成。其语法为:
--patch-module <module>=<file>(<pathsep><file>)*
其中<module>
是模块名称,<file>
是模块定义的文件系统路径名,<pathsep>
是主机平台的路径分隔符。
该--patch-module
选项可以多次使用,但对于任何特定模块名称最多使用一次。每个实例的作用是改变模块系统在指定模块中搜索类型的方式。在检查实际模块(无论是系统的一部分还是在模块路径上定义的)之前,它首先按顺序检查为选项指定的每个模块定义。补丁路径命名了一系列模块定义,但它不是模块路径,因为它具有泄漏的、类似类路径的语义。这允许测试工具,例如