跳到主要内容

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

模块路径

javacjlink、 和命令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选项允许访问指定包的公共类型。有时需要更进一步,通过核心反射 APIsetAccessible 方法启用对所有非公共元素的访问。可以在运行时使用该选项来执行此操作。它与选项具有相同的语法:--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选项可以多次使用,但对于任何特定模块名称最多使用一次。每个实例的作用是改变模块系统在指定模块中搜索类型的方式。在检查实际模块(无论是系统的一部分还是在模块路径上定义的)之前,它首先按顺序检查为选项指定的每个模块定义。补丁路径命名了一系列模块定义,但它不是模块路径,因为它具有泄漏的、类似类路径的语义。这允许测试工具,例如,将多个测试注入到同一个包中,而不必将所有测试复制到单个目录中。

--patch-module选项不能用于替换module-info.class文件。如果module-info.class在补丁路径上的模块定义中找到文件,则会发出警告并且该文件将被忽略。

如果在补丁路径上的模块定义中找到的包尚未由该模块导出或打开,那么它仍然不会被导出或打开。它可以通过反射 API 或--add-exports--add-opens选项显式导出或打开。

--patch-module选项替换了-Xbootclasspath:/p已删除的选项(见下文)。

--patch-module选项仅用于测试和调试。强烈建议不要在生产环境中使用它。

编译时间

编译javac器实现上述适用于编译时的选项:--module-source-path--upgrade-module-path--system--module-path--add-modules--limit-modules--add-reads--add-exports--patch-module

编译器以三种模式之一运行,每种模式都实现附加选项。

  • -source当由、-target--release选项定义的编译环境小于或等于 8 时,启用_传统模式。不能使用上述任何模块化选项。_

在遗留模式下,编译器的行为方式与 JDK 8 中的行为方式基本相同。

  • --module-source-path当编译环境为9或更高版本并且不使用该选项时,启用_单模块模式_。可以使用上述其他模块化选项;现有选项-bootclasspath-Xbootclasspath-extdirs-endorseddirs、 和-XXuserPathsFirst可能无法使用。

单模块模式用于编译以传统包分层目录树组织的代码。它是简单使用表单遗留模式的自然替代品

$ javac -d classes -classpath classes -sourcepath src Foo.java

module-info.java如果在命令行上指定了或文件形式的模块描述符module-info.class,或者在源路径或类路径中找到了该模块描述符,则源文件将被编译为该描述符命名的模块的成员,并且该模块将被编译为唯一的根模块。否则,如果--module <module>存在该选项,则源文件将被编译为 的成员<module>,这将是根模块。否则,源文件将被编译为未命名模块的成员,并且将如上所述计算根模块。

在此模式下,可以将任意类和 JAR 文件放在类路径上,但不建议这样做,因为这相当于将这些类和 JAR 文件视为正在编译的模块的一部分。

  • --module-source-path当编译环境为9或更高版本并且使用该选项时,启用_多模块模式_。还必须使用现有的-d选项来命名输出目录;可以使用上述其他模块化选项;现有选项-bootclasspath-Xbootclasspath-extdirs-endorseddirs、 和-XXuserPathsFirst可能无法使用。

多模块模式用于编译一个或多个模块,其源代码放置在模块源路径上的分解模块目录中。在此模式下,类型的模块成员资格由其源文件在模块源路径中的位置确定,因此命令行上指定的每个源文件必须存在于该路径的元素内。根模块集是为其指定至少一个源文件的模块集。

与其他模式相反,在此模式下必须通过选项指定输出目录-d。输出目录将被构造为模块路径的元素,,它将包含分解模块目录,这些目录本身包含类和资源文件。如果编译器在模块源路径上找到一个模块,但找不到该模块中某种类型的源文件,那么它将在输出目录中搜索相应的类文件。

在大型系统中,特定模块的源代码可能分布在几个不同的目录中。例如,在 JDK 本身中,模块的源文件可以在目录、或中的任一目录中找到,其中是目标操作系统的名称。为了在模块源路径中表达这一点,同时保留模块标识,我们允许此类路径的每个元素使用大括号 (和) 括起逗号分隔的替代列表,并使用单个星号 ( ) 代表模块名称。 JDK 的模块源路径可以写为src/<module>/share/classes``src/<module>/<os>/classes``build/gensrc/<module>``<os>``{``}``*

{src/*/{share,<os>}/classes,build/gensrc/*}

在这两种模块化模式下,编译器默认都会生成与模块系统相关的各种警告;这些可以通过选项禁用-Xlint:-module。通过选项的exportsopensrequires-automaticrequires-transitive-automatic键可以更精确地控制这些警告-Xlint

新选项--module-version <version>可用于指定正在编译的模块的版本字符串。

类文件属性

JDK 特定的类文件属性ModuleTarget可以选择记录目标操作系统和包含它的模块描述符的体系结构。其格式为:

ModuleTarget_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 os_arch_index; // index to a CONSTANT_utf8_info structure
}

常量池中的 UTF-8 字符串的os_arch_index格式为<os>-<arch>,其中<os>通常为linuxmacossolaris或之一windows,并且<arch>通常为x86amd64sparcv9arm或之一aarch64

打包:模块化 JAR 文件

jar无需更改即可使用该工具来创建模块化 JAR 文件,因为模块化 JAR 文件只是module-info.class根目录中带有一个文件的 JAR 文件。

jar工具实现了以下新选项,以允许在打包模块时将附加信息插入到模块描述符中:

  • --main-class=<class-name>,或者-e <class-name>简称,导致<class-name>被记录在module-info.class文件中作为包含模块public static void main入口点的类。 (这不是一个新选项;它已经在 JAR 文件的清单中记录了主类。)

  • --module-version=<version>导致作为模块的版本字符串<version>记录在文件中。module-info.class

  • --hash-modules=<pattern>导致在一组特定的可观察模块中依赖于该模块的特定模块的内容的哈希值被记录在文件中,module-info.class以供以后在依赖项验证中使用。仅记录名称与正则表达式匹配的模块的哈希值<pattern>。如果使用此选项,则还必须使用该---module-path选项(或-p简称)来指定可观察模块集,以便计算依赖于该模块的模块。

  • --describe-module,或者-d简称,显示指定 JAR 文件的模块描述符(如果有)。

jar工具的--help选项可用于显示其命令行选项的完整摘要。

定义了两个新的特定于 JDK 的 JAR 文件清单属性,以对应于 和--add-exports命令--add-opens行选项:

  • Add-Exports: <module>/<package>( <module>/<package>)*
  • Add-Opens: <module>/<package>( <module>/<package>)*

每个属性的值都是以空格分隔的斜杠分隔的模块名称/包名称对的列表。属性<module>/<package>值中的一对Add-Exports与命令行选项具有相同的含义--add-exports <module>/<package>=ALL-UNNAMED。属性<module>/<package>值中的一对Add-Opens与命令行选项具有相同的含义--add-opens <module>/<package>=ALL-UNNAMED

每个属性在文件的主要部分中最多可以出现一次MANIFEST.MF。特定对可以多次列出。如果指定的模块未解析,或者指定的包不存在,则忽略相应的对。这些属性仅在应用程序的主可执行 JAR 文件中解释,即在为-jarJava 运行时启动器的选项指定的 JAR 文件中;它们在所有其他 JAR 文件中都会被忽略。

打包:JMOD文件

新的 JMOD 格式超越了 JAR 文件,还包括本机代码、配置文件和其他类型的数据,这些数据如果有的话,自然不适合 JAR 文件。 JMOD文件用于打包JDK本身的模块;如果需要,开发人员还可以使用它们来打包自己的模块。

JMOD 文件可以在编译时和链接时使用,但不能在运行时使用。一般来说,为了在运行时支持它们,我们需要准备好即时提取和链接本机代码库。这在大多数平台上都是可行的,尽管它可能非常棘手,而且我们还没有看到很多需要此功能的用例,因此为了简单起见,我们选择限制此版本中 JMOD 文件的实用性。

新的命令行工具jmod可以用来创建、操作和检查 JMOD 文件。其一般语法为:

$ jmod (create|extract|list|describe|hash) <options> <jmod-file>

对于create子命令,<options>可以包含上述工具的--main-class--module-version--hash-modules和选项,还可以:---module-path``jar

  • --class-path <path>指定一个类路径,其内容将被复制到生成的 JMOD 文件中。

  • --cmds <path>指定一个或多个包含要复制的本机命令的目录。

  • --config <path>指定一个或多个包含要复制的配置文件的目录。

  • --exclude <pattern-list>指定要排除的文件,其中是以逗号分隔的、或<pattern-list>形式的模式列表。<glob-pattern>``glob:<glob-pattern>``regex:<regex-pattern>

  • --header-files <path>指定一个或多个包含要复制的 C 和 C++ 头文件的目录。

  • --legal-notices <path>指定一个或多个包含要复制的法律声明的目录。

  • --libs <path>指定一个或多个包含要复制的本机库的目录。

  • --man-pages <path>指定一个或多个包含要复制的手册页的目录。

  • --target-platform <os>-<arch>指定目标操作系统和体系结构,记录在文件ModuleTarget的属性中module-info.class

extract子命令接受单个选项 ,--dir以指示应将指定 JMOD 文件的内容写入的目录。如果该目录不存在,则会创建该目录。如果不存在此选项,则内容将被提取到当前目录中。

子命令list列出指定JMOD文件的内容;该describe子命令显示指定 JMOD 文件的模块描述符,其格式与和命令--describe-module的选项相同。这些子命令不接受任何选项。jar``java

hash子命令可用于对现有的 JMOD 文件集进行哈希处理。它需要--module-path--hash-modules选项。

jmod工具的--help选项可用于显示其命令行选项的完整摘要。

JEP 282jlink中描述了命令行链接工具 的详细信息。从高层次来看,它的一般语法是:

$ jlink <options> ---module-path <modulepath> --output <path>

其中该---module-path选项指定链接器要考虑的可观察模块集,该--output选项指定将包含生成的运行时映像的目录的路径。另一个<options>可以包括如上所述的---limit-modules---add-modules选项,以及附加的链接器特定选项。

jlink工具的--help选项可用于显示其命令行选项的完整摘要。

运行

HotSpot 虚拟机实现上述适用于运行时的选项:--upgrade-module-path--module-path--add-modules--limit-modules--add-reads--add-exports--add-opens--patch-module。这些选项可以传递给命令行启动器,java也可以传递给JNI 调用 API

特定于此阶段并受启动器支持的附加选项是:

  • --module <module>,或-m <module>简称,指定模块化应用程序的主模块。这将是用于构建应用程序的初始模块图的默认根模块。如果主模块的描述符不指示主类,则<module>/<class>可以使用该语法,其中<class>命名包含应用程序public static void main入口点的类。

启动器支持的其他诊断选项包括:

  • --list-modules显示可观察模块的名称和版本字符串,然后退出,方式与 相同java --version

  • --describe-module <module>,或者-d <module>简称,以与选项jar -djmod describe子命令相同的格式显示指定模块的模块描述符,然后退出。

  • --validate-modules验证所有可观察的模块,检查冲突和其他潜在错误,然后退出。

  • --dry-run初始化虚拟机并加载主类,但不调用main方法;这对于验证模块系统的配置很有用。

  • --show-module-resolution使模块系统在构造初始模块图时描述其活动。

  • -Dsun.reflect.debugModuleAccessChecks每当 API 中的访问检查java.lang.reflectIllegalAccessExceptionInaccessibleObjectException.当隐藏失败的根本原因时,这对于调试很有用,因为异常被捕获并且不会重新抛出。

  • -Xlog:module+[load|unload][=[debug|trace]]当模块在运行时模块图中定义和更改时,使 VM 记录调试或跟踪消息。这些选项在启动期间生成大量输出。

  • -verbose:module是 的简写-Xlog:module+load -Xlog:module+unload

  • -Xlog:init=debug如果模块系统初始化失败,则会显示堆栈跟踪。

  • --version--show-version--help和分别显示与现有、、和选项--help-extra相同的信息和工作方式,不同之处在于它们将帮助文本写入标准输出流而不是标准错误流。-version``-show-version``-help``-Xhelp

运行时为异常生成的堆栈跟踪已扩展为包括相关模块的名称和版本字符串(如果存在)。异常的详细字符串(例如ClassCastExceptionIllegalAccessException、 和 )IllegalAccessError也已更新以包含模块信息。

现有-jar选项已得到增强,因此如果正在启动的 JAR 文件的清单文件包含属性Launcher-Agent-Class,则 JAR 文件将作为应用程序和该应用程序的代理启动。这允许java -jar foo.jar用来代替更冗长的java -javaagent:foo.jar -jar foo.jar.

轻松强封装

在此版本中,按照 Java SE 9 平台规范的允许,默认情况下放宽了某些 JDK 包的强封装。这种放松是在运行时由新的启动器选项 控制的,--illegal-access其工作原理如下:

  • --illegal-access=permit打开运行时映像中每个模块中的每个包,以在所有未命名模块中进行编码,,如果该包存在于 JDK 8 中,则在类路径上进行编码。这可以实现静态访问(_即_通过编译的字节码)和深度访问通过平台的各种反射 API 进行反射访问。

    对任何此类包的第一次反射访问操作都会导致发出警告,但此后不会发出警告。此单个警告描述了如何启用进一步的警告。该警告无法被抑制。

    此模式是 JDK 9 中的默认模式。它将在未来版本中逐步淘汰并最终删除。

  • --illegal-access=warn与 相同,permit只是为每个非法反射访问操作发出警告消息。

  • --illegal-access=debug与 相同warn,只是针对每个非法反射访问操作都会发出警告消息和堆栈跟踪。

  • --illegal-access=deny禁用所有非法访问操作,但由其他命令行选项启用的操作除外,例如--add-opens.

    此模式将成为未来版本中的默认模式。

deny成为默认的非法访问模式时,permit可能会在至少一个版本中保持支持,以便开发人员可以继续迁移他们的代码。随着时间的推移, permitwarn、 和模式debug将被删除,--illegal-access选项本身也将被删除。 (为了启动脚本兼容性,在发出警告后,不支持的模式很可能会被忽略。)

默认模式--illegal-access=permit旨在让您意识到类路径上的代码至少会反射访问某些 JDK 内部 API 一次。为了为将来做好准备,您可以使用warndebug模式来了解所有此类访问。对于类路径上需要非法访问的每个库或框架,您有两种选择:

  • 如果组件的维护者已经发布了一个不再使用 JDK 内部 API 的新的固定版本,那么您可以考虑升级到该版本。

  • 如果组件仍然需要修复,那么我们鼓励您联系其维护者,并要求他们使用适当的导出 API(如果有)替换 JDK 内部 API。

如果您必须继续使用需要非法访问的组件,那么您可以通过使用一个或多个--add-opens选项来仅打开那些需要访问的内部包来消除警告消息。

要验证您的应用程序是否已为未来做好准备,请与--illegal-access=deny任何必要的--add-opens选项一起运行它。任何剩余的非法访问错误很可能是由于编译代码对 JDK 内部 API 的静态引用造成的。您可以通过jdeps使用该选项运行该工具来识别这些内容--jdk-internals。 (运行时系统不会针对非法静态访问操作发出警告,因为这需要深入的虚拟机更改并降低性能。)

检测到非法反射访问操作时发出的警告消息具有以下形式:

WARNING: Illegal reflective access by $PERPETRATOR to $VICTIM

在哪里:

  • $PERPETRATOR 是包含调用相关反射操作的代码的类型的完全限定名称以及代码源(即 JAR 文件路径)(如果可用),并且

  • $VICTIM 是一个字符串,描述正在访问的成员,包括封闭类型的完全限定名称

在默认模式下--illegal-access=permit,最多会发出其中一条警告消息,并附有附加说明文本。下面是运行 Jython 的示例:

$ java -jar jython-standalone-2.7.0.jar
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by jnr.posix.JavaLibCHelper (file:/tmp/jython-standalone-2.7.0.jar) to method sun.nio.ch.SelChImpl.getFD()
WARNING: Please consider reporting this to the maintainers of jnr.posix.JavaLibCHelper
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Jython 2.7.0 (default:9987c746f838, Apr 29 2015, 02:25:11)
[OpenJDK 64-Bit Server VM (Oracle Corporation)] on java9
Type "help", "copyright", "credits" or "license" for more information.
>>> ^D

运行时系统会尽最大努力抑制对同一 PERPETRATORPERPETRATOR 和 VICTIM 的重复警告。

一个扩展的例子

假设我们有一个应用程序模块 ,com.foo.bar它依赖于一个库模块 ,com.foo.baz。如果我们在 module-path 目录中有两个模块的源代码src

src/com.foo.bar/module-info.java
src/com.foo.bar/com/foo/bar/Main.java
src/com.foo.baz/module-info.java
src/com.foo.baz/com/foo/baz/BazGenerator.java

然后我们可以将它们一起编译:

$ javac --module-source-path src -d mods $(find src -name '*.java')

输出目录mods是一个模块路径目录,包含两个模块的分解、编译定义:

mods/com.foo.bar/module-info.class
mods/com.foo.bar/com/foo/bar/Main.class
mods/com.foo.baz/module-info.class
mods/com.foo.baz/com/foo/baz/BazGenerator.class

假设该类com.foo.bar.Main包含应用程序的入口点,我们可以按原样运行这些模块:

$ java -p mods -m com.foo.bar/com.foo.bar.Main

或者,我们可以将它们打包成模块化 JAR 文件:

$ jar --create -f mlib/com.foo.bar-1.0.jar \
--main-class com.foo.bar.Main --module-version 1.0 \
-C mods/com.foo.bar .
$ jar --create -f mlib/com.foo.baz-1.0.jar \
--module-version 1.0 -C mods/com.foo.baz .

mlib目录是一个模块路径目录,包含两个模块的打包、编译定义:

$ ls -l mlib
-rw-r--r-- 1501 Sep 6 12:23 com.foo.bar-1.0.jar
-rw-r--r-- 1376 Sep 6 12:23 com.foo.baz-1.0.jar

我们现在可以直接运行打包的模块:

$ java -p mlib -m com.foo.bar

jtreg增强功能

jtreg 测试工具支持新的声明性标签 ,@modules以表达测试对被测试系统中模块的依赖关系。它需要一系列以空格分隔的参数,每个参数都可以采用以下形式

  • <module>,其中<module>是模块名称,表示指定的模块必须存在;

  • <module>/<package>,指示指定的模块必须存在并且指定的包必须导出到测试的模块;或者

  • <module>/<package>:<flag>,指示指定的模块必须存在,如果该标志为open,则必须将指定的包打开到测试的模块,否则,如果该标志为,+open则必须将指定的包导出并打开到测试的模块。

一组默认@modules参数将用于目录层次结构中不包含此类标记的所有测试,可以将其指定为文件或任何文件中的modules属性TEST.ROOTTEST.properties

现有@compile标签接受新选项/module=<module>。这具有javac使用上面定义的选项进行调用的效果--module <module>,以将指定的类编译为指示模块的成员。

类加载器

Java SE Platform API 历史上指定了两个类加载器:引导类加载器(从引导类路径加载类)和系统类加载器(新类加载器的默认委托父类,通常用于执行以下操作)加载并启动应用程序。该规范没有规定这些类加载器的具体类型,也没有规定它们的精确委托关系。

自 1.2 版本以来,JDK 实现了三级类加载器层次结构,其中每个加载器委托给下一个加载器:

  • 应用程序类加载器( 的实例java.net.URLClassLoader)从类路径加载类,并作为系统类加载器安装,除非通过系统属性指定了备用系统加载器java.system.class.loader

  • 扩展类加载器也是 的一个实例,它加载通过扩展机制URLClassLoader可用的类,以及 JDK 内置的一些资源和服务提供者。 (Java SE 平台 API 规范中没有明确提及此加载器。)

  • 引导类加载器仅在虚拟机内实现并在 APInull中表示为ClassLoader,它从引导类路径加载类。

JDK 9 保留了这个三级层次结构,以保持兼容性,同时进行以下更改来实现模块系统:

  • 应用程序类加载器不再是内部类的实例URLClassLoader,而是内部类的实例。它是既不是 Java SE 也不是 JDK 模块的命名模块的默认加载器。

  • 扩展类加载器不再是内部类的实例URLClassLoader,而是内部类的实例。它不再通过扩展机制加载类,该机制已被JEP 220删除。然而,它确实定义了选定的 Java SE 和 JDK 模块,下面将详细介绍这些模块。在其新角色中,该加载器被称为_平台类加载器_,可以通过新ClassLoader::getPlatformClassLoader 方法使用它,并且 Java SE 平台 API 规范将要求它。

  • 引导类加载器在库代码和虚拟机中实现,但为了兼容性,它仍然在APInull中表示ClassLoader。它定义了核心 Java SE 和 JDK 模块。

保留平台类加载器不仅是为了兼容性,也是为了提高安全性。引导类加载器加载的类型被隐式授予所有安全权限 ( AllPermission),但其中许多类型实际上并不需要所有权限。我们通过将不需要所有权限的模块定义到平台加载器而不是引导类加载器,并通过在默认安全策略文件中授予它们实际需要的权限来取消_特权_模块。定义到平台类加载器的 Java SE 和 JDK 模块是:

java.activation*            jdk.accessibility
java.compiler* jdk.charsets
java.corba* jdk.crypto.cryptoki
java.scripting jdk.crypto.ec
java.se jdk.dynalink
java.se.ee jdk.incubator.httpclient
java.security.jgss jdk.internal.vm.compiler*
java.smartcardio jdk.jsobject
java.sql jdk.localedata
java.sql.rowset jdk.naming.dns
java.transaction* jdk.scripting.nashorn
java.xml.bind* jdk.security.auth
java.xml.crypto jdk.security.jgss
java.xml.ws* jdk.xml.dom
java.xml.ws.annotation* jdk.zipfs

(这些列表中的星号'*'表示可升级模块。)

提供工具或导出工具API的JDK模块被定义到应用程序类加载器:

jdk.aot                     jdk.jdeps
jdk.attach jdk.jdi
jdk.compiler jdk.jdwp.agent
jdk.editpad jdk.jlink
jdk.hotspot.agent jdk.jshell
jdk.internal.ed jdk.jstatd
jdk.internal.jvmstat jdk.pack
jdk.internal.le jdk.policytool
jdk.internal.opt jdk.rmic
jdk.jartool jdk.scripting.nashorn.shell
jdk.javadoc jdk.xml.bind*
jdk.jcmd jdk.xml.ws*
jdk.jconsole

所有其他 Java SE 和 JDK 模块都定义到引导类加载器:

java.base                   java.security.sasl
java.datatransfer java.xml
java.desktop jdk.httpserver
java.instrument jdk.internal.vm.ci
java.logging jdk.management
java.management jdk.management.agent
java.management.rmi jdk.naming.rmi
java.naming jdk.net
java.prefs jdk.sctp
java.rmi jdk.unsupported

三个内置类加载器协同工作来加载类,如下所示:

  • 应用程序类加载器首先搜索为所有内置加载器定义的命名模块。如果为这些加载器之一定义了合适的模块,则该加载器将加载该类。如果在定义给这些加载器之一的命名模块中找不到类,则应用程序类加载器将委托给其父级。如果其父类未找到某个类,则应用程序类加载器将搜索类路径。在类路径中找到的类将作为该加载程序的未命名模块的成员加载。

  • 平台类加载器搜索为所有内置加载器定义的命名模块。如果为这些加载器之一定义了合适的模块,则该加载器将加载该类。 (因此,平台类加载器现在可以委托给应用程序类加载器,当升级模块路径上的模块依赖于应用程序模块路径上的模块时,这会很有用。)如果在命名模块中找不到类定义到这些加载器之一,然后平台类加载器委托给其父级。

  • 引导类加载器搜索为其自身定义的命名模块。如果在引导加载程序定义的命名模块中找不到类,则引导类加载程序将搜索通过选项添加到引导类路径的文件和目录-Xbootclasspath/a。在引导类路径中找到的类将作为该加载程序的未命名模块的成员加载。

应用程序和平台类加载器委托给它们各自的父加载器,以确保当在定义到内置加载器之一的模块中找不到类时仍然搜索引导类路径。

删除:引导类路径选项

在早期版本中,该-Xbootclasspath选项允许覆盖默认引导类路径,并且该-Xbootclasspath/p选项允许将一系列文件和目录添加到默认路径之前。该路径的计算值通过 JDK 特定的系统属性报告sun.boot.class.path

模块系统就位后,引导类路径默认为空,因为引导类是从各自的模块加载的。编译javac器仅支持-Xbootclasspath旧模式下的选项,java启动器不再支持这两个选项,并且系统属性sun.boot.class.path已被删除。

--system如上所述,编译器的选项可用于指定系统模块的备用源,并且其-release选项可用于指定备用平台版本,如JEP 247(针对旧平台版本进行编译)中所述。在运行时--patch-module,上面提到的选项可用于将内容注入到初始模块图中的模块中。

相关选项-Xbootclasspath/a允许将文件和目录附加到默认引导类路径。此选项以及java.lang.instrument包中的相关 API 有时由检测代理使用,因此为了兼容性,它在运行时仍然受支持。如果指定了它的值,则通过 JDK 特定的系统属性报告jdk.boot.class.path.append。该选项可以传递给命令行启动器,java也可以传递给 JNI 调用 API。

测试

许多现有的测试都受到模块系统引入的影响。在 JDK 9 中@modules,如上所述,标记已根据需要添加到单元和回归测试中,并且使用该-Xbootclasspath/p选项或假设系统类加载器的测试URLClassLoader已更新。

当然,模块系统本身有一套广泛的单元测试。在JDK 9源森林中,运行时测试位于存储库的test/jdk/modulesjdk目录和存储库的runtime/moduleshotspot目录中;编译时测试位于存储库的tools/javac/moduleslangtools目录中。

包含此处描述的更改的早期访问版本在模块系统的整个开发过程中都可用。强烈鼓励 Java 社区成员针对这些构建测试他们的工具、库和应用程序,以帮助识别兼容性问题。

风险和假设

该提案的主要风险是由于现有语言结构、API 和工具的更改而导致的兼容性风险。

主要由于Java 平台模块系统 (JSR 376)的引入而导致的变化包括:

  • public修饰符应用于 API 元素不再保证该元素可以随处访问。现在,可访问性还取决于包含该元素的包是否由其定义模块导出或打开,以及包含尝试访问该元素的代码的模块是否可以读取该模块。例如,以下形式的代码可能无法正常工作:

    Class<?> c = Class.forName(...);
    if (Modifier.isPublic(c.getModifiers()) {
    // Assume that c is accessible
    }
  • 如果在命名模块和类路径中都定义了包,则类路径上的包将被忽略。因此,类路径不能再用于扩充内置于环境中的包。包javax.transactioneg)是由java.transaction模块定义的,因此不会在类路径中搜索该包中的类型。此限制对于避免跨类加载器和跨模块拆分包非常重要。在编译时和运行时,升级模块路径可用于升级环境中内置的模块。该--patch-module选项可用于其他临时修补。

  • 这些ClassLoader::getResource*方法不能再用于定位类文件以外的 JDK 内部资源。模块私有的非类资源可以通过JEP 220Class::getResource*中定义的方法、Module::getResourceAsStream方法或jrt:URL 方案和文件系统来读取。

  • java.lang.reflect.AccessibleObject::setAccessible 方法不能用于访问未由其定义模块导出或打开的包的公共成员,也不能用于访问未由其定义模块打开的包的非公共成员;无论哪种情况,InaccessibleObjectException都会抛出一个异常。如果框架库(例如序列化程序)需要在运行时访问此类成员,则必须通过声明包含模块打开、声明该包打开或打开该包来向框架模块打开相关包。通过--add-opens命令行选项打包。

  • JVM TI 代理无法再检测在运行时环境启动早期运行的 Java 代码。ClassFileLoadHook特别是,在原始阶段不再发送该事件。该VMStart事件标志着启动阶段的开始,仅在 VM 初始化到可以加载除 之外的模块中的类后才会发布java.base。两个新功能can_generate_early_class_hook_eventscan_generate_early_vmstart,可以由精心编写的代理添加,以在 VM 初始化早期处理事件。更多细节可以在类文件加载钩子事件启动事件的更新描述中找到。

  • 安全策略文件中的语法==已被修改,以增加授予标准和 JDK 模块的权限,而不是覆盖它们。因此,覆盖 JDK 默认策略文件其他方面的应用程序不需要复制授予标准和 JDK 模块的默认权限。

定义 Java EE API 或主要与 Java EE 应用程序相关的 API 的模块已被弃用,并将在未来版本中删除。默认情况下,类路径上的代码不会解析它们:

  • 未命名模块的默认根模块集基于模块java.se而不是java.se.ee模块。因此,默认情况下,未命名模块中的代码将无法访问以下模块中的 API:

    java.activation
    java.corba
    java.transaction
    java.xml.bind
    java.xml.ws
    java.xml.ws.annotation

    这是一个有意的选择,虽然很痛苦,但有两个目标:

    • 避免与在某些相同包中定义类型的流行库发生不必要的冲突。广泛使用的_eg_jsr305.jar,在包中定义了注解类型,它也是由模块定义的。javax.annotation``java.xml.ws.annotation

    • 为了使现有应用程序服务器更容易迁移到 JDK 9。应用程序服务器通常会覆盖这些模块中的一个或多个模块的内容,并且在短期内它们最有可能通过继续放置必要的非模块化 JAR 来实现这一点类路径上的文件。如果默认解析这些模块,那么应用程序服务器的维护者将不得不采取尴尬的操作来排除它们,以便覆盖它们。

    这些模块仍然是 JDK 9 的一部分。根据需要,可以通过 选项授予类路径上的代码访问这些模块中的一个或多个模块的权限--add-modules

一些 Java SE API 的运行时行为已发生变化,但方式仍然遵循其现有规范:

  • java.net.URLClassLoader如上所述,应用程序和平台类加载器不再是类的实例。调用ClassLoader::getSystemClassLoader并盲目地将结果转换为 的现有代码URLClassLoader,或者对该类加载器的父类执行相同操作的现有代码可能无法正常工作。

  • 某些 Java SE 类型已被取消特权,现在由平台类加载器而不是引导类加载器加载,如上所述。直接委托给引导类加载器的现有自定义类加载器可能无法正常工作;它们应该被更新以委托给平台类加载器,这可以通过新ClassLoader::getPlatformClassLoader 方法轻松获得。

  • java.lang.Package由内置类加载器为命名模块中的包创建的实例没有规范或实现版本。在以前的版本中,此信息是从 的清单中读取的rt.jar。期望Package::getSpecification*orPackage::getImplementation*方法始终返回非空值的现有代码可能无法正常工作。

有一些与源代码不兼容的 Java SE API 更改:

  • java.lang包包括两个新的顶级类,Module以及ModuleLayer.该java.lang包是按需隐式导入的(import java.lang.*)。如果现有源文件中的代码按需导入某个其他包,并且该包声明了ModuleorModuleLayer类型,并且现有代码引用该类型,则该文件将无法在不进行更改的情况下进行编译。

  • java.lang.instrument.Instrumentation接口声明了两个新的抽象方法,redefineModuleisModifiableModule。该接口不打算在模块外部实现java.instrument。如果有外部实现,那么它们将无法在不进行更改的情况下在 JDK 9 上编译。

  • transform接口中声明的五参数方法java.lang.instrument.ClassFileTransformer现在是默认方法。该接口现在还声明了一个新transform方法,该方法可以在加载时检测类时使相关java.lang.reflect.Module对象可供转换器使用。现有的已编译代码将继续运行,但使用现有五参数变换方法作为功能接口的现有源代码将不再编译。

最后,由于 JDK 特定的 API 和工具的修订而导致的更改包括:

  • 大多数 JDK 的内部 API 在编译时默认是不可访问的,如JEP 260中所述。在先前版本中针对这些 API 编译并带有警告的现有代码将不再编译。解决方法是通过--add-exports上面定义的选项来破坏封装。

  • sun.misc和包中选定的关键内部 APIsun.reflect已移至jdk.unsupported模块,如 JEP 260 中所述。这些包中的非关键内部 API(例如sun.misc.BASE64{De,En}coder)已被移动或删除。

  • 如果存在安全管理器,则需要运行时权限accessSystemModules才能通过ClassLoader::getResource*Class::getResource*方法访问 JDK 内部资源;在以前的版本中,${java.home}/lib/rt.jar需要读取该文件的权限。

  • 如上所述,-Xbootclasspath和选项已被删除。-Xbootclasspath/p在编译时,新--release选项可用于指定备用平台版本(请参阅JEP 247)。在运行时,如上所述的新--patch-module选项可用于将内容注入系统模块。

  • sun.boot.class.path由于引导类路径默认为空,因此JDK 特定的系统属性已被删除。使用此属性的现有代码可能无法正常工作。

  • JEP 179@jdk.Exported引入的JDK 特定注释已被删除,因为它传达的信息现在记录在模块描述符的声明中。我们没有看到 JDK 之外的工具使用此注释的证据。exports

  • META-INF/services先前在相应的系统模块中找到的资源文件和rt.jar其他内部工件不存在于相应的系统模块中,因为服务提供者和依赖项现在在模块描述符中声明。扫描此类文件的现有代码可能无法正常工作。

  • file.encoding与以前一样,可以通过选项在命令行上设置JDK 特定的系统属性-D,但它必须指定基本模块中定义的字符集。如果指定任何其他字符集,则运行时系统将无法启动。指定此类字符集的现有启动脚本可能无法正常工作。

  • 默认情况下,不能再使用APIcom.sun.tools.attach将代理附加到当前进程或当前进程的祖先。可以通过jdk.attach.allowAttachSelf在命令行上设置系统属性来启用此类附件操作。

  • 在未来的版本中,JVM TI 代理的动态加载将默认禁用。为了准备该更改,我们建议允许动态代理的应用程序开始使用该选项-XX:+EnableDynamicAgentLoading来显式启用该加载。该选项-XX:-EnableDynamicAgentLoading禁用动态代理加载。

依赖关系

JEP 200(模块化 JDK)最初在 XML 文档中定义了 JDK 中存在的模块,作为临时措施。该 JEP 将这些定义移动到正确的模块描述符,_即_和文件,并且根源代码存储库中的文件被删除module-info.javamodule-info.class``modules.xml

JDK 9 中JEP 220(模块化运行时映像)的初始实现使用自定义构建时工具来构造 JRE 和 JDK 映像。这个 JEP 用这个工具取代了那个工具jlink

根据JEP 238 ,模块化 JAR 文件也可以是多版本 JAR 文件。