JEP 201:模块化源代码
概述
将 JDK 源代码重新组织为模块,增强构建系统以编译模块,并在构建时强制执行模块边界。
非目标
这个 JEP 定义了 JDK 的一种新的源代码布局。这种布局可以在 JDK 之外使用,但本 JEP 的目标并不是设计一种广泛认可的通用模块化源代码布局。
动机
Project Jigsaw 旨在为 Java SE 平台设计并实现一个标准的模块系统,并将该系统应用到平台本身和 JDK 中。其主要目标是使平台的实现更容易扩展到小型设备,提高安全性和可维护性,提升应用程序的性能,并为开发者提供更好的大规模编程工具。
重新组织源代码的动机包括:
-
让 JDK 开发人员有机会熟悉系统的模块化结构;
-
通过在构建中强制执行模块边界来保留这个结构,甚至在引入模块系统之前;以及
-
使 Project Jigsaw 的开发能够顺利进行,而不必总是将当前非模块化的源代码“重组”为模块化形式。
描述
当前方案
目前,大部分 JDK 源代码的组织方式大致可以追溯到 1997 年。简略来说:
src/{share,$OS}/{classes,native}/$PACKAGE/*.{java,c,h,cpp,hpp}
其中:
-
share
目录包含共享的、跨平台的代码; -
$OS
目录包含特定操作系统的代码,其中$OS
是solaris
、windows
等之一; -
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
举一个更复杂的例子,java.lang.ProcessImpl
和 ProcessEnvironment
这两个包私有类的源代码是与操作系统相关的;对于类 Unix 系统,它们位于以下三个文件中:
src/solaris/classes/java/lang/ProcessImpl.java
ProcessEnvironment.java
native/java/lang/ProcessEnvironment_md.c
(是的,尽管此代码与所有 Unix 衍生系统相关,但第二级目录仍被命名为 solaris
;更多信息见下文。)
src/{share,$OS}
下有一些目录与当前结构不匹配,包括:
Directory Content
-------------------------- --------------------------
src/{share,$OS}/back JDWP back end
bin Java launcher
instrument Instrumentation support
javavm Exported JVM include files
lib Files for $JAVA_HOME/lib
transport JDWP transports
新方案
JDK 的模块化提供了一个罕见的机会,可以彻底重组源代码以使其更易于维护。我们在 JDK 代码库森林的每个代码库中实施以下方案,hotspot
除外。简要形式如下:
src/$MODULE/{share,$OS}/classes/$PACKAGE/*.java
native/include/*.{h,hpp}
$LIBRARY/*.{c,cpp}
conf/*
legal/*
哪里:
-
$MODULE 是一个模块名称(例如,
java.base
); -
share
目录包含共享的、跨平台的代码,与之前相同; -
$OS
目录包含特定操作系统的代码,与之前相同,其中$OS
是unix
、windows
等之一; -
classes
目录包含 Java 源文件和资源文件,这些文件按照反映其 API$PACKAGE
层次结构的目录树组织,与之前相同; -
native
目录包含 C 或 C++ 源文件,与之前相同,但组织方式有所不同:-
include
目录包含用于外部使用的 C 或 C++ 头文件(例如,jni.h
); -
C 或 C++ 源文件放置在
$LIBRARY
目录中,该目录的名称是编译后的代码将被链接到的共享库或 DLL 的名称(例如,libjava
或libawt
);最后,
-
-
conf
目录包含供最终用户编辑的配置文件(例如,net.properties
)。 -
legal
目录包含法律声明。
重新构建前面的示例,java.lang.Object
类的源代码布局如下所示:
src/java.base/share/classes/java/lang/Object.java
native/libjava/Object.c
包私有的 java.lang.ProcessImpl
和 ProcessEnvironment
类的源代码布局方式如下:
src/java.base/unix/classes/java/lang/ProcessImpl.java
ProcessEnvironment.java
native/libjava/ProcessEnvironment_md.c
(我们借此机会,最终将 solaris
目录重命名为 unix
。)
当前 src/{share,$OS}
目录下不符合当前结构的内容现在已放入相应的模块中:
Directory Module
-------------------------- --------------------------
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
目录中不打算由最终用户编辑的文件现在是资源文件。
构建系统更改
构建系统现在一次编译一个模块,而不是一次编译一个代码库,并且它根据模块图的逆拓扑排序来编译模块。在可能的情况下,相互之间没有直接或间接依赖关系的模块会并行编译。
编译模块而非存储库的一个附带好处是,corba
、jaxp
和 jaxws
存储库中的代码可以利用新的 Java 语言特性和 API。这在以前是被禁止的,因为这些存储库是在 jdk
存储库之前编译的。
中间态(即非镜像)构建中的编译类被划分为多个模块。如今我们有:
jdk/classes/*.class
修订后的构建系统产生:
jdk/modules/$MODULE/*.class
镜像构建的结构没有改变,内容方面只有非常细微的差异。
模块边界在构建时由构建系统尽可能地强制执行。如果违反了模块边界,那么构建将会失败。
替代方案
还有许多其他可能的源码布局方案,包括:
-
将
{share,$OS}
保留在顶部,并创建一个modules
目录来存放模块类文件:src/{share,$OS}/modules/$MODULE/$PACKAGE/*.java
native/include/*.{h,hpp}
$LIBRARY/*.{c,cpp}
conf/* -
将所有内容放在相应的
$MODULE
目录下,但仍然将{share,$OS}
保留在顶部:src/{share,$OS}/$MODULE/classes/$PACKAGE/*.java
native/include/*.{h,hpp}
$LIBRARY/*.{c,cpp}
conf/* -
按照当前提案,将
{share,$OS}
推入$MODULE
目录中,但移除中间的classes
目录,并在native
和conf
目录名称前加上下划线_
,以简化纯 Java 模块的常见情况:src/$MODULE/{share,$OS}/$PACKAGE/*.java
_native/include/*.{h,hpp}
$LIBRARY/*.{c,cpp}
_conf/* -
方案 3 的变体,但将
{share,$OS}
保留在顶部:src/{share,$OS}/$MODULE/$PACKAGE/*.java
_native/include/*.{h,hpp}
$LIBRARY/*.{c,cpp}
_conf/* -
方案 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 的范围。 -
有些模块包含多个本地库;我们是否应该将它们合并,使得每个模块最多只有一个本地库? —— 不需要;在某些情况下,我们需要每个模块支持多个本地库的灵活性,例如,“无头 (headless)”与“有头 (headful)”AWT 的情况。
测试
如前所述,此 JEP 不会改变 JRE 和 JDK 二进制映像的结构,而只会对内容进行细微的更改。因此,我们通过将使用此更改构建的映像与未使用此更改构建的映像进行比较,并运行测试以验证实际的细微更改,来验证这一更改。
风险与假设
我们假设 Mercurial 能够处理实现这一变更所需的大量文件重命名操作,同时在过程中保留所有的历史信息。早期的测试表明 Mercurial 能够做到这一点,但仍存在一个小风险,即某些文件新旧位置之间的关系可能未被正确记录。在这种情况下,文件在其旧位置的历史记录仍将保存在代码库中;只是会更难以查找。
无法将针对使用旧方案的存储库创建的补丁直接应用到使用新方案的存储库上,反之亦然。为了解决这个问题,我们开发了一个脚本,将补丁中的文件名从旧位置转换为新位置。
依赖
此 JEP 是 Project Jigsaw 的几个 JEP 中的第二个。它包含了从 JEP 200 中定义的 JDK 模块化结构,但并不显式依赖于该 JEP。