Skip to content

JEP 201: Modular Source Code | 模块化源代码

摘要

将 JDK 源代码重新组织为模块,增强构建系统以编译模块,并在构建时强制执行模块边界。

非目标

本 JEP 不会更改 JRE 和 JDK 二进制映像的结构,也不会引入模块化系统。该工作由相关的 JEP 220261 来完成。

本 JEP 为 JDK 定义了一种新的源代码布局。这种布局可以在 JDK 之外使用,但本 JEP 的目标不是设计一个广泛接受的通用模块化源代码布局。

动机

Project Jigsaw 的目标是为 Java SE 平台设计和实现一个标准的模块化系统,并将该系统应用于平台本身和 JDK。其主要目标是使平台的实现更容易针对小型设备进行扩展、提高安全性和可维护性、提高应用程序性能,并为开发人员提供更好的大规模编程工具。

重新组织源代码的动机包括:

  1. 给 JDK 开发人员提供了解系统模块化结构的机会;

  2. 通过在构建中强制执行模块边界,保留该结构,即使在引入模块化系统之前也是如此;

  3. 使得 Project Jigsaw 的开发能够进行,而无需总是将现有的非模块化源代码转换为模块化形式。

描述

当前方案

目前,大部分 JDK 源代码大致上按照 1997 年的方案组织。简化形式如下:

src/{share,$OS}/{classes,native}/$PACKAGE/*.{java,c,h,cpp,hpp}

其中:

  • share目录包含共享的跨平台代码;

  • $OS目录包含特定于操作系统的代码,其中$OSsolariswindows等之一;

  • classes目录包含 Java 源文件和可能的资源文件;

  • native目录包含 C 或 C++ 源文件;

  • $PACKAGE是相关的 Java API 包名称,其中的句点被斜杠替换。

以一个简单的例子来说,jdk存储库中java.lang.Object类的源代码包括一个 Java 文件和一个 C 文件:

src/share/classes/java/lang/Object.java
          native/java/lang/Object.c

对于一个不那么简单的例子,package-private 的java.lang.ProcessImplProcessEnvironment类的源代码是特定于操作系统的;对于类 Unix-like 的系统,它们位于三个文件中:

src/solaris/classes/java/lang/ProcessImpl.java
                              ProcessEnvironment.java
            native/java/lang/ProcessEnvironment_md.c

(是的,第二级目录的名称为solaris,即使这段代码与所有 Unix 衍生操作系统相关;下面会更详细地说明。)

src/{share,$OS}下有几个目录与当前结构不匹配,包括:

目录                          内容
--------------------------    --------------------------
src/{share,$OS}/back          JDWP后端
                bin           Java启动器
                instrument    仪器支持
                javavm        导出的JVM包含文件
                lib           $JAVA_HOME/lib目录下的文件
                transport     JDWP传输

新方案

JDK 的模块化为完全重构源代码提供了难得的机会,以便更容易进行维护。除了hotspot之外,我们在 JDK 存储库中的每个存储库中实现以下方案。简化形式如下:

src/$MODULE/{share,$OS}/classes/$PACKAGE/*.java
                        native/include/*.{h,hpp}
                               $LIBRARY/*.{c,cpp}
                        conf/*
                        legal/*

其中:

  • $MODULE 是模块名(例如java.base);

  • share目录包含共享的跨平台代码,与之前一样;

  • $OS目录包含特定于操作系统的代码,与之前一样,其中$OSunixwindows等之一;

  • classes目录包含 Java 源文件和资源文件,按照反映其 API $PACKAGE层次结构的目录树进行组织,与之前一样;

  • native目录包含 C 或 C++ 源文件,与之前类似,但组织方式有所不同:

    • include目录包含 C 或 C++ 头文件,用于外部使用(例如jni.h);

    • C 或 C++ 源文件放置在$LIBRARY目录中,其名称与编译后的代码将被链接到的共享库或 DLL 的名称相同(例如libjavalibawt);

  • conf目录包含供最终用户编辑的配置文件(例如net.properties)。

  • legal目录包含法律声明。

重新设计之前的示例,java.lang.Object 类的源代码布局如下:

src/java.base/share/classes/java/lang/Object.java
                    native/libjava/Object.c

java.lang.ProcessImplProcessEnvironment 两个包私有类的源代码布局如下:

src/java.base/unix/classes/java/lang/ProcessImpl.java
                                     ProcessEnvironment.java
                   native/libjava/ProcessEnvironment_md.c

(我们在这里有机会,最终将 solaris 目录重命名为 unix。)

当前与当前结构不符的 src/{share,$OS} 目录中的内容现在位于适当的模块中:

目录                          模块
--------------------------    --------------------------
src/{share,$OS}/back          jdk.jdwp.agent
                bin           java.base
                instrument    java.instrument
                javavm        java.base
                lib           $MODULE/{share,$OS}/conf
                transport     jdk.jdwp.agent

当前 lib 目录中不打算由最终用户编辑的文件现在是资源文件。

构建系统更改

构建系统现在一次只编译一个模块而不是一个仓库,并根据模块图的反向拓扑排序来编译模块。在可能的情况下,并行编译彼此不直接或间接依赖的模块。

将模块编译而不是仓库的一个附带好处是,corbajaxpjaxws 仓库中的代码可以使用新的 Java 语言功能和 API。在之前是被禁止的,因为这些仓库在 jdk 仓库之前编译。

在非打包(非镜像)构建中编译的类被分成模块。当前我们有:

jdk/classes/*.class

修订后的构建系统会生成:

jdk/modules/$MODULE/*.class

如上所述,打包构建的结构不变;其内容只有非常细微的差异。

构建系统尽可能在构建时通过构建系统强制模块边界。如果违反了模块边界,构建将失败。

备选方案

有许多其他可能的源代码布局方案,包括:

  1. 在顶层保留 {share,$OS},使用 modules 目录来包含模块类文件:

    src/{share,$OS}/modules/$MODULE/$PACKAGE/*.java
                    native/include/*.{h,hpp}
                           $LIBRARY/*.{c,cpp}
                    conf/*
    
  2. 将所有内容放在适当的 $MODULE 目录下,但保留 {share,$OS} 在顶层:

    src/{share,$OS}/$MODULE/classes/$PACKAGE/*.java
                            native/include/*.{h,hpp}
                                   $LIBRARY/*.{c,cpp}
                            conf/*
    
  3. {share,$OS} 下推到当前提案中所示的 $MODULE 目录中,但删除中间的 classes 目录,并在 nativeconf 目录的名称前添加下划线,以简化纯 Java 模块的常见情况:

    src/$MODULE/{share,$OS}/$PACKAGE/*.java
                            _native/include/*.{h,hpp}
                                    $LIBRARY/*.{c,cpp}
                            _conf/*
    
  4. 方案 3 的一个变种,但 {share,$OS} 在顶层:

    src/{share,$OS}/$MODULE/$PACKAGE/*.java
                            _native/include/*.{h,hpp}
                                    $LIBRARY/*.{c,cpp}
                            _conf/*
    
  5. 方案 3 的另一种变种,进一步将 {share,$OS} 下推,以简化没有 $OS 特定代码的纯 Java 模块的情况:

    src/$MODULE/$PACKAGE/*.java
                _native/include/*.{h,hpp}
                        $LIBRARY/*.{c,cpp}
                _conf/*
                _$OS/$PACKAGE/*.java
                    _native/include/*.{h,hpp}
                            $LIBRARY/*.{c,cpp}
                    _conf/*
    

我们拒绝了使用下划线的方案(3-5)因为它们太不熟悉和难以导航。我们比起方案 1 和方案 2 更喜欢目前的提案,因为它与当前方案的改变最小,同时将模块的所有源代码放在单个目录下。依赖于当前方案的工具和脚本必须进行修改,但至少对于 Java 源代码来说,在每个 $MODULE 目录下的结构与之前相同。

我们考虑的其他问题:

  • 我们是否应该定义用于资源文件的不同目录,以使其与 Java 源文件分开? — 不需要,这似乎不值得麻烦。
  • 某些模块具有跨多个仓库的内容;这是一个问题吗? — 这是一个麻烦事,但构建系统可以通过“VPATH”机制来处理它。随着时间的推移,我们可能会重新构造这些仓库,以减少或甚至消除跨仓库模块,但这超出了此 JEP 的范围。
  • 某些模块具有多个本机库;我们是否应该合并它们,使每个模块最多只有一个本机库? — 不需要,因为在某些情况下,我们需要每个模块具有多个本机库的灵活性,例如,“无头”与“有头”AWT。

测试

如上所述,此 JEP 不会改变 JRE 和 JDK 二进制映像的结构,只对其内容进行轻微更改。因此,我们通过比较使用此更改构建的映像与未使用此更改构建的映像,并运行测试以验证实际的轻微更改来验证此更改。

风险和假设

我们假设 Mercurial 能够处理实施此更改所需的大量文件重命名操作,并在此过程中保留所有历史信息。初步测试表明 Mercurial 能够做到这一点,但还存在一个小风险,即某些文件的新旧位置之间的关系可能没有正确记录。在这种情况下,文件在其旧位置的历史将仍然存在于存储库中;只是更难找到。

无法将针对使用旧方案的存储库创建的补丁直接应用于使用新方案的存储库,反之亦然。为了缓解这个问题,我们开发了一个脚本,将补丁中的文件名从旧位置转换为新位置。

依赖关系

此 JEP 是Project Jigsaw的几个 JEP 之一。它结合了JEP 200中 JDK 的模块化结构的定义,但与该 JEP 不明确地有依赖关系。