JEP261 模块系统

作者  Alan Bateman, Alex Buckley, Jonathan Gibbons, Mark Reinhold

所有者 Mark Reinhold

创建  2014/10/23 15:05

更新  2017/03/08 13:58

类型  功能

状态  已集成

域   SE

JSR 376

讨论  拼图的开发在 openjdk.java.net

努力  XL

持续时间  L

优先  1

检验人 Alan Bateman, Alex Buckley, Chris Hegarty, Jonathan Gibbons, Mandy Chung, Paul Sandoz

支持  Brian Goetz

发行  9

版本  8061792


JEP 282: jlink: The Java Linker

JEP 200: The Modular JDK

依赖
JEP 220: Modular Run-Time Images

JEP 260: Encapsulate Most Internal APIs

概述

实现 JSR376 所描述的 Java 平台模块系统,以及 JDK 相关的更改与增强。

描述

Java 平台模块系统(JSR 376) 提出了针对 Java 编程语言,Java 虚拟机以及标准 Java API 的修改与扩展建议。此 JEP 将对这一规范进行实现。而这一任务带来的相应结果,就是 javac 编译器,HotSpot 虚拟机和运行时库将会把模块作为一种新的 Java 编程组件予以实现,并为开发活动的所有阶段提供模块的可靠配置和强有力的封装。

此 JEP 还将改变,扩展并添加与编译、链接、执行相关的 JDK 特有的工具与 API 。这些工具和 API 并不属于 JSR 所规定的范围。对于其它工具与 API 的更改,例如 javadoc 工具与 Doclet API,将在单独的 JEP 中进行讨论。

此 JEP 假设读者熟悉最新的模块化系统的状态文档,以及以下拼图项目的 JEP:

阶段

除了我们已经熟悉的编译时(javac)和运行时(java 运行时启动器),我们加入了一个新概念:链接时。这是一个处于编译时与运行时之间的阶段,在这一阶段,一组模块可被组装并优化到一个自定义的运行时镜像中。用于进行链接的工具,jlink,是 JEP282 讨论的主题。另外许多新加入的 javac 与 java 命令行选项,也是由 jlink 来实现的。

模块路径

javac,jlink 和 java,以及其它一些命令,现在将接受一个新的用于定义多个模块路径的选项。模块路径是一个序列,该序列中的每个元素要么是一个模块的定义,要么是一个包含模块定义的目录。所谓模块定义,是以下二者之一:

  • 一个模块工件,例如一个包含了编译好的模块定义的模块化的 JAR 文件或 JMOD 文件,或
  • 一个 exploded-module directory(这个不知道怎么翻),按照约定,此模块的名称和内容是一个反映包继承层次的“exploded”(这个也不知道怎么翻)目录树。

在以上第二种情况中,目录树可以是一个编译好的模块定义,其中存放有单独的类与资源文件,以及根目录下的一个 module-info.class 文件,或是在编译时,一个源模块定义,其中存放有单独的源文件,并在根目录下有一个 module-info.java 文件。

一个模块路径,和其它类型的路径一样,是一组路径名字符串,其中的多个路径名由所在系统的路径分隔符分隔(大部分系统上这个符号是 ‘:’,windows上是 ‘;’)。

模块路径与类路径有很大的差别:类路径是定位单独的类型与资源的手段,而模块路径则是定位整个完整模块的手段。类路径的每个元素是一个类型与资源定义的容器,比如,要么是一个 JAR 文件,要么是一个 exploded,包继承层次目录树(an exploded, package-hierarchical directory tree)。而每个模块路径的元素,则是一个模块定义,或一个底下的每个元素都是一个模块定义的目录,例如,一个类型与资源定义的容器:要么是一个模块化 JAR 文件,要么是一个 JMOD 文件,又或者是一个 exploded 模块目录。

在解析过程中,模块系统基于当前阶段,通过搜寻各个不同路径和依赖来定位模块。另外模块系统还会按照以下顺序搜寻构建到环境中的已编译模块:

  • 包含以源文件形式存在的模块定义的编译模块路径(只在编译时,由命令行参数 –module-source-path 指定)
  • 包含用于替代构建到环境中的可升级模块的已编译好的模块定义的升级模块路径(编译时与运行时,由 –upgrade-module-path 指定)
  • 系统模块是已编译好的,构建到环境中的模块(编译时与运行时)。一般来说这包括 Java SE 和 JDK 模块,但如果是自定义链接镜像,还会包含库和应用模块。在编译时系统模块可以通过 -system 选项进行覆盖,用于指向一个用来加载系统模块的 JDK 镜像。
  • 包含已编译好的库和应用模块(所有阶段)的应用模块路径(由 –module-path 指定,或简写为 -mp)。在链接时此路径还可以包含 Java SE 和 JDK 模块。

被这些路径所呈现的模块定义,和系统模块一起,共同构成可观察模块。

当在一个模块路径中对一个模块进行搜索时,模块系统会取第一个找到的名字符合的模块。如果名字包含版本字符串,这个字符串会被忽略。如果模块路径里某一元素中包含有多个名字相同的模块,解析将会失败,编译器,链接器和虚拟机将报告错误并退出。避免版本冲突应该由构建工具和容器应用通过正确配置模块路径来避免,模块系统的实现目标中并不包含解决版本选择问题。

根模块

模块系统对模块图的构造,依据的是对一组根模块可传递闭包的依赖性与可观测模块间关系的解析。(The module system constructs a module graph by resolving the transitive closure of the dependences of a set of root modules with respect to the set of observable modules.)

当编译器对未命名模块中的代码进行编译,或当 java 启动器被调用,应用程序的主类从类路径中被加载到应用程序类加载器的未命名模块中时,未命名模块的默认根模块集合将通过如下规则得出:

  • java.se 模块如果存在,将会是一个根模块。如果 java.se 不存在,那么每一个在升级模块路径下,或在系统模块中且无条件导出至少一个包的 java.* 模块会是一个根模块。
  • 每一个在升级模块路径下,或在系统模块中且无条件导出至少一个包的 non-java.* 模块会是一个根模块。

否则,默认根模块集合将取决于代码所处的阶段:

  • 在编译时,就是所有被编译的模块(下面将会谈到更多)
  • 在链接时,此集合为空
  • 在运行时,是应用程序的在启动时通过 –module (简写为 -m)启动参数指定的主模块。

在少数情况下,需要将某些专属的平台、库或服务提供者模块添加到默认根模块集合中,以确保它们会在模块图中被呈现。在任何阶段,–add-modules <模块名>(,<模块名>)* 将把指定名称的模块添加到默认根模块集合中。

作为一个运行时的特例,如果 <模块名> 被设置为 ALL-DEFAULT, 那么未命名模块的默认根模块集合将被添加到根模块集合中。当应用程序是一个容器,而运行于其上的应用程序依赖与容器应用依赖不同时,这一设置将会有帮助。

而作为一个未来的运行时的特例,如果 <模块名> 被设置为 ALL-SYSTEM,那么无论是否在默认集合中,所有系统模块将会被添加到根模块集合。在某些时候这是测试套件的需要。但这一设置将会导致很多模块被解析,所以一般来说,ALL-DEFAULT 应被优先考虑使用。

还有最后一个特例,如果 <模块名>是 ALL-MODULE-PATH,那么所有在相关模块路径下的可观测模块将会被添加到根模块集合中。ALL-MODULE-PATH 在编译时和运行时都可用。这一设置主要是提供给诸如 Maven 的构建工具使用的,这些工具已经确保在所有在模块路径中的模块都是必要的。这一设置同样也可以作为一种自动添加模块到根模块结合的简单的办法。

限制可观测模块

有时候需要对可观测模块进行限制,例如调试代码,或当主模块是应用程序类加载器定义的未命名模块时,减少被解析的模块数量。参数 –limit-modules 可被用于任何阶段,以达到这一目的。其语法如下:
–limit-modules <模块名>(,<模块名>)*
此选项的效果是限制在具名模块、主模块、以及任何由 –add-module 加载的模块的的传递闭包上的可观测模块数量。
(–limit-module 参数计算出来的传递闭包只是用于计算被限制的可观测模块的一个临时结果。解析器在之后将会被再次调用以计算实际的模块图)

增加可读性

在进行测试与调试的时候,有时会需要让一个模块去读取另一个模块,即便该模块并未在模块声明中通过 requires 语句依赖另一个模块。例如,让一个被测试的模块访问测试套件,或访问测试套件相关的类库。–add-reads 选项可以用来在编译时和运行时做这个事情。它的语法如下:
--add-reads <源模块名>=<目标模块名>

–add-reads 可以被使用不止一次。每调用一次这个选项会增加一条源模块到目标模块的可读性边。本质上,这就是一个模块声明中 requires 语句的命令行形式,或是一次  Module::addReads 方法的无限制形式调用。如此一来,只要源模块中每个包都在模块定义中被通过 exports 语句、或调用 Module::addExports 方法,或 –add-exports 方法进行了导出,源模块中的代码就可以对目标模块中包底下的类型进行访问。

例如,假设一个测试套件将一个白盒测试类注入到了 java.management 模块,而这个类扩展并导出了(假象的)testng 模块中的工具类,那么其对访问权限的需求可通过如下参数实现:
--add-reads java.management=testng

作为特例,如果 <目标模块>取值是 ALL-UNNAMED, 那么会由源模块出发,与每一个当前与未来的未命名模块建立一条可读性边,包括相应类路径下的模块。这一做法使得模块中尚未被转化为模块形式的代码也可以被测试框架进行测试。

打破封装

有时候,需要违反由模块系统规定并由编译器和虚拟机强制应用的访问控制边界,以便让一个模块访问另一个模块的未导出类型。这在例如允许针对内部类型的白盒测试,或向对不被支持的内部 API 有依赖的代码暴露这些 API 时是有必要的。–add-exports 选项可以在编译时和运行时被使用以达到这一目的。其语法如下:
--add-exports <源模块名>/<包名>=<目标模块名>(,<目标模块名>)*

–add-exports 选项可以被多次使用,但对相同的源模块名与包名组合只能使用一次。每调用一次该参数,就会增加一个从源模块到目标模块的针对指定包的合规导出。这一选项实际上就是模块定义中 exports 语句,或 Module::addExports 方法的无限制形式调用的命令行形式。如此一来,只要目标模块中每个包都在模块定义中被通过 requires 语句、或调用 Module::addReads 方法,或 –add-reads 进行了引用,目标模块中的代码就可以对目标模块中指定包底下的类型进行访问。

例如,假设模块 jmx.wbtest 包含针对 java.management 模块未导出包 com.sun.jmx.remote.internal 的白盒测试,那么必要的访问权限可以使用如下选项赋予:
--add-exports java.management/com.sun.jmx.remote.internal=jmx.wbtest

作为特例,如果<目标模块名>取值是 ALL-UNNAMED,那么源包会被导出到所有已被创建或之后将被创建的未命名模块。由此, 给所有类路径代码赋予对 java.management 模块 sun.management 包的访问权限可以通过以下参数来设置:
--add-exports java.management/sun.management=ALL-UNNAMED

–add-export 参数必须谨慎使用。你确实可以使用它来获取针对内部 API,或类库模块,甚至 JDK 的访问权限,但你必须了解其中的风险:如果内部 API 被改变或移除了,那么你的类库或应用程序将会因此无法运行。

给模块内容打补丁

在测试与调试的时候,有时能够使用替代代码或实验版本的代码替换模块下某些类或资源,或者提供全新版本的类文件,资源甚至包会对任务很有帮助。这一工作可以通过选项 –patch-module 在运行时和编译时实现。其语法如下:
--patch-module <模块名>=<模块定义文件路径>(<路径分隔符><模块定义文件路径>)*

–patch-module 选项可以被多次使用,但针对相同的模块只能使用一次。每一个参数调用会改变模块系统如何在某一指定模块中查找类型。在去某一指定模块中查找之前,无论这个模块是系统的一部分,或在模块路径中定义,模块系统都会先按顺序查找被参数中的模块定义文件所定义的模块。补丁路径配置了一系列模块定义文件,但它不是模块路径,因为它具有像类路径一样的易泄露的语义。这一参数的使用,让测试套件在无需把所有测试拷贝到一个路径的前提下,把多个测试注入到同一个包中成为可能。

–patch-module 参数不能用来替换 module-info.class 文件。如果在参数指定的模块定义文件中发现了 module-info.class 文件,那么系统将会抛出一个警告,而该文件会被忽略。

如果在补丁路径中的模块定义文件下的某个包并未被该模块所导出,那么补丁中的包也不会被导出。如果需要,可以使用反射 API 或 –add-exports 选项进行显式的导出。

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

–patch-module 选项是为了测试与调试设计的。我们不提倡在生产环境中使用此参数。

编译时

javac 编译器实现了以上所述的选项,使它们在编译时可用:–module-source-path, –upgrade-module-path, -system, –module-path, –add-modules, –limit-modules, –add-reads, –add-exports, and –patch-module。

编译器有三种运行模式,每一种都支持额外的选项:

  • 遗留模式在由 -source、-target 和 -release 选项定义的编译环境小于等于8时启用。任何上述模块化相关选项均不可用。

在遗留模式下编译器的行为与 JDK 8 时一模一样。

  • 单模块模式在编译环境大于等于 9 时启用,但 –module-source-path 选项不可用。上述其余选项可用。现有选项 -bootclasspath,-Xbootclasspath,-extdirs,-endorseddirs 和 -XXuserPathsFirst 可用。

单模块模式用于编译使用传统包继承目录树组织的代码。它是对以下形式遗留模式语句的替代:
$ javac -d classes -classpath classes -sourcepath src Foo.java
如果在命令行,或源文件路径/类路径中发现了以 module-info.java 或 module-info.class 文件形式存在的模块描述,那么源文件会被编译成为模块定义所描述的模块的成员,此模块则会成为唯一的根模块。而如果 –module <模块名> 选项被使用了,源文件会被编译成为 <模块名> 所指定模块的成员,而指定模块会成为根模块。如果前两者都没有发生,源文件将会被编译成未命名模块的成员,而根模块将会根据前面的描述计算得出。

在此模式下,往类路径中放任意的类文件和 JAR 文件是可以的,但并不建议这么做,因为这相当于把这些类文件和 JAR 文件当做正被编译的模块的一部分来对待。

  • 多模块模式在编译环境大于等于 9 并且使用了 –module-source-path 选项时启用。用于指定输入目录的参数 -d 也必须同时被使用。上述其余模块化选项此时也可用,而 -bootclasspath, -Xbootclasspath, -extdirs, -endorseddirs 和 -XXuserPathsFirst 不可用。

多模块模式用于编译源代码存放在模块源路径的 exploded-module 目录下的一个或多个模块。此模式下某一类型所属的模块是由它的源文件在模块源路径的位置所确定的,所以在命令行中指定的每一个源文件必须存在于源路径下的某个元素中。至少指定了一个源文件的模块的集合会作为根目录集合。

与其它模式所不同的是,在多模块模式下,必须使用 -d 对输出目录进行指定。输出目录将会被作为模块路径的元素之一进行架构,也就是说,它会包含内含类和资源文件的 exploded-module 目录。如果编译器在模块源路径中发现了一个模块,但模块中某些类型的源文件却无法被找到,编译器将会去输出目录下对相关类文件进行查找。

在大型系统中,某个模块的源代码可能会散布在多个不同的目录下。例如,对于 JDK 来说,一个模块的源文件也许会存在于以下任何一个目录中:src/<模块名>/share/classes, src/<模块名>/<目标操作系统名>/classes, or build/gensrc/<模块名>。为了在模块源路径中表达这些源代码的分布情况的同时能够保留模块的特征,我们允每个这样的路径元素使用花括号”{}“修饰逗号分隔的可选项列表,以及使用星号“*” 表示模块名。这样一来 JDK 的模块源路径就可以写做:

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

打包:模块化 JAR 文件

jar 工具可以像以前版本的用法一样用来创建模块化 JAR 文件,因为一个模块化 JAR 文件就是一个在根目录下拥有 module-info.class 的 JAR 文件而已。

jar 工具实现了下列新的选项,以便在模块被打包的时候往模块描述中插入额外的信息:

  • –main-class=<类名>,简写为 -e <类名>,会将包含模块入口 public static void main 方法的<类名>记录到 module-info.class 文件中。
  • –module-version=<版本号>会将<版本号>作为模块版本字符串写入到 module-info.class 文件中。
  • –hash-modules=<正则表达式> 将某一组可观测模块下依赖于当前模块的模块的内容哈希值记录到 module-info.class 文件中,以供随后校验依赖关系时使用。只有模块名满足<正则表达式>的模块的哈希值会被记录。如果此选项被使用到,那么 –module-path 选项也必须同时被使用以指定可观测模块的集合,以便计算出依赖于当前模块的模块。

另外,新选项 –print-module-descriptor,简写为 -p,(如果模块描述存在的话)将显示 JAR 文件中模块的描述。

jar 工具的 –help 选项可以被用于获取完整的命令行选项概要。

打包:JMOD 文件

新的 JMOD 格式用于包含本地代码、配置文件以及其它无法放到 JAR 文件中去的数据种类。在打包 JDK 模块的时候就用上了 JMOD 文件。如果开发者愿意的话,JMOD 文件亦可被用来打包他们的模块。JMOD 文件的最终格式目前仍未确定,但目前它是基于 ZIP 文件的。JMOD 文件可以被用在编译时与链接时,但不能被用于运行时。如果要在运行时支持 JMOD 文件,大体而言,我们需要做好实时的提取并链接本地代码库的准备。这在大部分平台上是可行的,但会十分麻烦,并且我们也并没有见过太多需要这一能力的使用场景。所以为简单起见我们选择在这一版本中限制 JMOD 的使用范围。

一个新的命令行工具 jmod 可被用于创建 JMOD 文件,或打印一个 JMOD 文件的内容。其语法大体如下:

$ jmod (create|list|describe) <选项> <jmod 文件名>

子命令 list 和 describe 没有任何选项。而子命令 create 支持选项 –main-class, –module-version, –hash-modules 以及 –module-path,这几项也是上面描述过的 jar 工具选项。另外,create 还支持:

  • –class-path <路径> 用于指定一个内容将会被拷贝到生成的 JMOD 文件中的类路径。
  • –cmds <路径> 用于指定一到多个包含将被拷贝到 JMOD 文件中的本地命令的目录。
  • –config <路径> 用于指定一到多个将要被拷贝到 JMOD 文件中的配置文件的目录。
  • –libs <路径> 用于指定一到多个包含将要被拷贝的本地库的目录。
  • –os-arch <操作系统架构> 用于指定将被记录在 module-info.class 文件中的操作系统架构。
  • –os-name <操作系统名> 用于指定将被记录在 module-info.class 文件中的操作系统名。
  • –os-version <版本号> 用于指定将被记录在 module-info.class 文件中的操作系统版本。

jmod 工具的 –help 选项用于获取其命令行选项的完整概要。

链接时

对命令行链接工具 jlink 的详细描述请参见JEP282。他的大体语法如下:

$ jlink <选项> ---module-path <模块路径> --output <路径>

–module-path 指定了一组要被链接器处理的可观测模块,–output 选项指定了用于输出运行时镜像的目录。其余选项包含了前面提到过的 –limit-modules 和 –add-modules,以及其它链接器专有的选项。

jlink 工具的 –help 选项用于获取其命令行选项的完整概要。

运行时

HotSpot 虚拟机为运行时实现了以上所描述的选项:–upgrade-module-path, –module-path, –add-modules, –limit-modules, –add-reads, –add-exports, and –patch-module。这些选项可以与命令行启动器 java 一起使用,也可以结合 JNI 调用 API 使用。

专门针对这一阶段的,被启动器支持的额外选项还有:

  • –module <模块名>,简写为 -m <模块名>,用于指定一个模块化系统的主模块。这个模块也会作为构建应用程序初始模块图的默认根模块。如果主模块描述没有指定一个包含 public static void main 应用入口的主类,那么语法 <模块名>/<类名> 可被用于指定这个类。
  • –list-modules 显示可观测模块的名字与版本字符串,然后退出,与 java -version 的行为相似。
  • –list-modules <模块名>(,<模块名>)* 显示指定模块的完整模块描述(如果此模块是可观测模块的话),然后退出。

启动器支持的额外选项包括:

  • -Xdiag:resolver 会使模块系统在构建初始模块图的同时描述它正在进行的活动。
  • -Dsun.reflect.debugModuleAccessChecks 会在任何对 java.lang.reflect API 的访问检查失败并抛出 IllegalAccessException 或 InaccessibleObjectException 的同时产生一个线程转储(thread dump)。这对于调试由于异常被捕捉但并没有重新抛出而导致底层的失败原因被隐藏的情况很有用。
  • -Xlog:modules=[debug|trace] 会使得虚拟机在模块于运行时模块图中被定义或更改时记录 debug 或 trace 信息。这一选项会导致启动时产生海量的日志输出。

运行时的异常跟踪堆栈被扩展了,以使他们能够呈现相关模块的名字与版本字符串(当存在时)。诸如 ClassCastException,IllegalAccessException, 和 IllegalAccessError 等异常的详细信息同样也被更新以包含模块信息。针对其它类型诊断信息的增强也在规划中。

一个详细的示例

假设我们有一个应用程序模块 com.foo.bar,它依赖于一个库模块 com.foo.baz。而两个模块的源代码都在模块路径下面:

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 是一个模块路径目录,包含有 exploded,编译好的两个模块的定义:

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 -mp 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 -mp mlib -m com.foo.bar

jtreg 增强

jtreg 测试套件支持一个新的声明式标签,@modules。它读取一系列的参数,每个参数的格式为 <模块名> 或 <模块名>/<包名>。不论参数是哪种格式,测试只会在指定的模块确实存在时被执行。第二种格式额外地要求测试可以访问在模块下的指定的包。如果该包未被导出,那么测试套件会自动将其导出到包含测试的模块下。

可以在一个 TEST.ROOT 文件,或任何 TEST.properties 文件中声明一组作为模块属性的默认 @modules 参数,这组参数会被用于所有该模块下的不具有 @modules 标签的测试。

目前已存在的 @compile 标签现在接受一个新的选项 module=<模块名>。这与上述的 javac 参数 –modules 具有相同效果,会把指定的类编译为指定模块的成员。

类加载器

Java SE 平台 API 具有两个类加载器:bootstrap 类加载器用于从 bootstrap 类路径加载类,而 系统类加载器 则是新建类加载器的委托父加载器,并且一般来说会是被用于加载与启动应用程序的类加载器。规范并没有强制规定这两个类加载器的具体类型,也没有规定他们的委托关系。

从 1.2 版开始,JDK 实现了一个三级继承关系的类加载器,每级类加载器之间是委托的关系:

  • 应用程序类加载器,一个 java.net.URLClassLoader 实例,作为默认系统类加载器从类路径加载类。除非 java.system.class.loader 系统参数指定了一个替代的系统类加载器。
  • 扩展类加载器,也是一个 java.net.URLClassLoader 实例,加载通过扩展机制提供的类,也同时加载内建于 JDK 的一些资源与服务提供者。(这一加载器并没有在 Java SE 平台 API 规范中被明确描述)。
  • bootstrap 类加载器从 bootstrap 类路径中加载类。这一加载器只在虚拟机内部实现,在 ClassLoader API 中以 null 表示。

为保证兼容性,在为实现模块系统做出以下变更的同时,JDK 9 保留了这种三级继承关系:

  • 应用程序类加载器不再是 URLClassLoader 的实例,而变成了一个内部(internal)类。它是既非 Java SE 亦非 JDK 模块的其它具名模块的默认加载器。
  • 扩展类加载器不再是 URLClassLoader 的实例,而变成了一个内部(internal)类。它不再通过已被JEP220移除的扩展机制加载类。不过它定义了某些 Java SE 与 JDK 模块,这在下面会进一步的讲解。在它的新角色下,它现在叫做平台类加载器。它可以通过 newClassLoader::getPlatformClassLoader 方法来获取。在 Java SE 平台 API 规范中,这个类加载器会成为一个必须的加载器。
  • bootstrap 类加载器将在类库代码和虚拟机二者中被实现,但出于兼容性考虑它仍然在 ClassLoader API 中被 null 所代表。它定义了核心的 Java SE 和 JDK 模块。

保留平台类加载器不仅出于兼容性考虑,也是为了增强安全性。被 boostrap 类加载器加载的类型隐式地被赋予所有安全权限(AllPermission),但很多这些类型并不真的需要所有权限。我们通过把一些并不需要所有权限的模块定义为通过平台类加载器而非 bootstrap 类加载器来加载,并在默认安全策略文件中赋予他们真正需要的权限,来给它们实现了降权处理。Java SE 和 JDK 中被定义为通过平台类加载器加载的模块有:

java.activation

java.annotations.common

java.compact1

java.compact2

java.compact3

java.compiler

java.corba

java.scripting

java.se

java.se.ee

java.sql

java.sql.rowset

java.transaction

java.xml.bind

java.xml.ws

jdk.accessibility

jdk.charsets

jdk.crypto.ec

jdk.crypto.pkcs11

jdk.dynalink

jdk.jsobject

jdk.localedata

jdk.naming.dns

jdk.scripting.nashorn

jdk.xml.dom

jdk.zipfs

除了提供工具,或导出工具 API 的模块被定义为通过应用程序加载器进行加载,所有其他 Java SE 和 JDK 模块都被定义为通过 bootstrap 进行加载。这些通过bootstrap加载器加载的模块有:

jdk.attach

jdk.compiler

jdk.hotspot.agent

jdk.internal.le

jdk.internal.opt

jdk.jartool

jdk.javadoc

jdk.jconsole

jdk.jdeps

jdk.jdi

jdk.jlink

jdk.jshell

jdk.jstatd

jdk.jvmstat

三个内建的类加载器以如下方式合作进行类的加载:

  • 应用程序类加载器首先搜索定义到所有内建类加载器中的模块。如果某个内建的加载器中找到了合适的模块,那么这个加载器将负责进行类加载。如果在一个定义在这些加载器下的模块中没能找到目标类,应用程序类加载器将把查找委托给父加载器。如果在父加载器中也没能找到这个类,应用程序类加载器将对类路径进行搜索。在类路径中找到的类将作为此类加载器未命名模块的成员进行加载。
  • 平台类加载器搜索定义到所有内建类加载器中的模块。如果某个内建的加载器中找到了合适的模块,那么这个加载器将负责进行类加载。(平台类加载器于是可以将查找委托给应用程序类加载器。当一个在升级模块路径下的模块依赖于一个在应用程序模块路径下的模块是,这一行为会很有用。)如果在一个定义在这些加载器下的模块中没能找到目标类,应用程序类加载器将把查找委托给父加载器。
  • bootstrap 类加载器搜索定义到它自身的具名模块。如果一个类无法在一个定义到 bootstrap 类加载器的具名模块中找到,bootstrap 加载器会搜索通过 -Xbootclasspath/a 选项添加到 bootstrap 类路径中的文件与目录。在 bootstrap 类路径中找到的类将会作为该加载器未命名模块的成员被加载。

应用程序与平台类加载器在一个类无法在定义到内建加载器的模块中找到的时候,把查找委托给他们的父加载器的目的,是为了保证 bootstrap 类路径会被搜索到。

移除:Bootstrap 类路径选项

在早期版本中,-Xbootclasspath 选项允许默认 bootstrap 类路径被改写,而 -Xbootclasspath/p 选项则允许一系列文件和目录被添加到默认路径前。该路径的计算结果通过 JDK 专有的系统属性 sun.boot.class.path 反馈出来。
有了模块系统后,bootstrap 类路径将默认为空,因为 bootstrap 类是从它们各自的模块中被加载的。javac 编译器仅在遗留模式中支持选项 -Xbootclasspath,而启动命令 java 则不再支持任何这类选项,系统属性 sun.boot.class.path 也被移除了。

前面曾提到过,编译器的 -system 选项可被用于指定一个备用的系统模块来源,而 JEP 247(为旧版本平台进行编译)中提到 -release 选项可被用于指定备用的平台版本。运行时选项 –patch-module 可被用于向在初始模块图中的模块注入内容。

一个相关选项,-Xbootclasspath/a,允许文件和目录被添加到默认 bootstrap 类路径后面。这一选项,以及在 java.lang.instrument 包中的相关 API,有时会被用于仪表代理(instrumentation agents),所以从兼容性考虑这个选项在运行时仍然被支持。如果指定了的话,其取值将通过 JDK 专有系统属性 jdk.boot.class.path.append 反馈出来。此选项可以被传递给命令行启动器 java,也可以被传递给 JNI 调用 API。

设计相关的一些问题

  • –add-modules 选项是否应允许指定被添加模块的类加载器?
  • 在 javac 的遗留模式下,module-info.java 源文件是被拒绝呢,还是被忽略?
  • 在 javac 的多模块模式下,-sourcepath 和 -classpath 选项让人困扰。是否要禁用它们?
  • jlink 是否应支持 –module 选项,或者采用一些其它方法指定应用入口,以及在链接时阶段产生的应用入口与命令行启动器之间的关系?
  • jlink,jar 和 jmod 工具使用 GNU 风格的选项,但 javac 和 Java 则不是。这一情况应该统一。
  • JMOD 文件的格式需要最终敲定。
  • 现有的被 jtreg 实现的 @library 与 @build 标签应被扩展,以能够与模块良好合作。

测试

随着对模块系统的引入,许多现存的测试将会受到影响。在 JDK 9 中,@modules 标签已被添加到超过 3000 个单元与集成测试中,另外还有很多使用 -Xbootclasspath/p 选项,或假设系统类加载器是 URLClassLoader 的测试已经被更新。

当然,模块系统本身就自带了大量的单元测试。在 JDK 9 源代码森林中,大部分运行时测试代码在 jdk 代码仓库的 test/jdk/jigsaw 目录,以及 hotspot 代码仓库的 runtime/modules 目录下。而大部分编译时测试代码在 lang 工具代码仓库的 tools/javac/modules 目录下。

包含了在这里描述的所有变更的早期预览版已经可以被获取了。我们鼓励 Java 社区广大成员们针对这些早起预览版对他们的工具、库和应用程序进行测试,以帮助我们发现任何遗留的兼容性问题。

风险与假设

本提案的主要风险在于因为对现存语言结构,API 和工具的更改所带来的兼容性问题。

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

  • 对一个 API 元素使用关键字 public 不再能够保证该元素在任何地方都可以访问。
    可访问性现在还取决于包含该元素的包是否被所在模块所导出,以及尝试调用该元素的代码所在的模块是否有访问该元素所在模块的权限。例如,以下形式的代码并不一定能正确工作:

Class<?> c = Class.forName(…);

if (Modifier.isPublic(c.getModifiers()) {

      // Assume that c is accessible

}

  • 如果一个包在一个具名模块以及类路径中同时被定义了,在类路径中的定义将被忽略。因此类路径不再能够用来覆盖环境内建的类型。例如,javax.transaction 包被 java.transaction 模块所定义,因此类路径不会作为搜索 javax.transaction 下面类型时的查找来源之一。这一限制对避免把包分割到不同的类加载器以及不同的模块中十分重要。在编译时和运行时,升级模块路径可用于对环境内建的模块进行升级。–patch-module 选项可以被用来加载其它特别的补丁。
  • ClassLoader::getResource* 和 Class::getResource 方法不再能够被用来读取 JDK 内部的资源。模块私有资源可通过 Module::getResourceAsStream 或 jrt: URL 架构以及在 JEP220 中定义的文件系统来读取。
  • java.lang.reflect.AccessibleObject::setAccessible 方法不能用于取得并没有被所在模块导出的包的访问权限,如果尝试这样做将会抛出 InaccessibleObjectException 异常。如果一个诸如序列化库这样的框架库需要访问这样的成员,那么相关包必须通过包定义中的导出声明或者 –add-exports 命令行选项来导出到框架的模块中。
  • JVM TI 代理不再能够对在运行时启动阶段早期的 Java 代码进行监测。具体而言,ClassFileLoadHook 事件不再会在原始阶段送出。标志启动阶段开始的 VMStart 事件只会在 VM 已经处于能够从除了 java.base 模块中的其它模块加载类的初始化阶段被送出。如果一个代理在编码实现的过程中认真处理了 VM 初始化早期阶段的事件,那么这个 agent 可以在实现中加入 cangenerateearlyclasshookevents 和 cangenerateearlyvmstart 这两个能力。更多细节可以在已被更新了的类文件加载钩子事件以及启动事件中被找到。

定义了 Java EE API,或者定义了主要为 Java EE 应用服务的 API 的模块默认不会为类路径中的代码进行解析:

  • 未命名模块的默认根模块集合是基于 java.se 模块,而不是 java.se.ee 模块的。因此,在未命名模块中的代码默认将不具备访问以下模块的能力:

    java.activation

    java.annotations.common

    java.corba

    java.transaction

    java.xml.bind

    java.xml.ws

这或许是个痛苦的选择,但这却是基于以下两点目的而有意为之:

  • 避免与在相同包名下定义了类型的常用库发生冲突。例如,被广泛使用的 jsr305.jar 在 javax.annotation 包下面定义的注解类型,同时也在 java.annotations.common 模块下存在。
  • 让现有的应用服务器更易于迁移到 JDK9。应用服务器通常会覆盖一到多个这些模块的内容,而这些应用服务器很有可能在短时间内继续把需要的非模块化 JAR 文件放在类路径下。如果这些(JavaEE)模块默认会被解析,那么应用服务器维护者将需要采取措施把它们排除,以便覆盖它们,这会很别扭。

一些 Java SE 的运行时行为发生了改变,不过它们仍然遵循规范的定义:

  • 如上所述,应用和平台类加载器不再是 java.net.URLClassLoader 类的实例。调用 ClassLoader::getSystemClassLoader,或尝试获取类加载器的父加载器, 并无条件将结果转化为 URLClassLoader 的代码将会无法正常工作。
  • 如上所述,一些 Java SE 类型被做了降权处理,它们现在由平台类加载器负责加载,而不是之前的 bootstrap 类加载器。现有的会将查找委托给 bootstrap 加载器的自定义类加载器可能因此无法正常工作;这些自定义加载器应被更新成为会将查找委托给平台类加载器。平台类加载器可以很容易地通过方法 ClassLoader::getPlatformClassLoader 获得。
  • 如果系统属性 java.security.policy 被用来覆盖,而非增加,系统的内建安全策略,那么用于替代的策略必须确保被降权的由平台类加载器加载的系统模块被赋予了所有需要的权限。

有一个源代码不兼容的 Java SE API 变更:

  • java.lang.instrument.ClassFileTransformer 接口中定义的接受五个参数的 transform 方法现在是一个默认方法。该接口目前也定义了一个新的 transform 方法,此方法会在加载时对类进行监测的时候,使相关 java.lang.reflect.Module 对象对转换器可用。现有的已编译的代码将可继续运行,但现有的使用了已有五参数 transform 方法作为功能接口的源代码将无法被编译。

总的来说,由于对 JDK 特有 API 以及工具进行修订而导致的变更包含:

  • 大部分 JDK 内部 API 默认不可访问,这一点在 JEP260 中有详细描述。依赖这些 API 的现有代码可能无法正常工作。一个解决方法是使用 –add-exports 打破封装。如 JEP260 所描述的,一些 sun.misc 和 sun.reflect 包下的关键内部 API 被移到了 jdk.unsupportedmodule 下。这些包下的非关键内部 API,诸如 sun.misc.BASE64{De,En}coder,被直接移除了。
  • -Xbootclasspath 和 -Xbootclasspath/p 选项被移除了。新加入的 -release 参数可在编译时被用于指定 替代的平台版本(见PEP247)。–patch-module 选项在运行时可被用于向系统模块中注入内容。
  • 因为 bootstrap 类路径默认是空的,JDK 特有的系统属性 sun.boot.class.path 被移除了。现有的使用该属性的代码可能无法正确工作。
  • JEP179 中引入的 JDK 特有的注解 @jdk.Exported 将被移除,因为其所传达的信息现在被记录在了模块描述的导出声明中。我们在 JDK 之外并没有发现使用该注解的工具。
  • 之前存在于 rt.jar 中的 META-INF/services 资源文件以及其它内部工件不会出现在相关的系统模块中,因为服务提供者和依赖关系现在在模块描述中声明。现有的会对这些文件进行搜寻的代码可能无法正确工作。
  • JDK 特有的系统属性 file.encoding 可如之前一般在命令行中通过 -D 选项进行设置,但它只有在指定的字符集在基础模块中被定义的情况下才会起作用。现有的指定了其它字符集的启动脚本可能无法正确工作。

和对模块化镜像的引入一样,在抽象层面确定这些变更带来的全面影响是不可能的。因此我们必须极大地依赖内部以及(特别是)外部的测试。如果某些变更被证明对于开发者、部署者或最终用户而言是不可逾越的障碍,我们将会研究减轻它们造成的影响的方法。

依赖

作为临时措施,JEP200(模块化JDK)最初使用 XML 的形式定义了在 JDK 中出现的模块。本 JEP 将这些定义移到了恰当的模块定义,如 module-info.java 和 module.info.class 文件之中,而在根源代码仓库中的 modules.xml 文件被彻底移除了。

JEP220(模块化运行时镜像)在 JDK9 中最初的实现使用了一个自定义的构建时工具来构架 JRE 和 JDK 镜像。本 JEP 实用 jlink 替代了自定义的工具。

根据 JEP238 的描述,模块化 JAR 文件也可以是多版本 JAR 文件。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: JEP261 模块系统

  • Trackback 关闭
  • 评论 (0)
  1. 暂无评论

return top