JEP 201:模块化源代码
概括
将 JDK 源代码重新组织为模块,增强构建系统以编译模块,并在构建时强制执行模块边界。
非目标
这个JEP没有改变JRE和JDK二进制镜像的结构,也没有引入模块系统。相关的 JEP 220和261涵盖了这项工作。
此 JEP 为 JDK 定义了新的源代码布局。此布局可以在 JDK 之外使用,但设计广泛接受的通用模块化源代码布局并不是此 JEP 的目标。
动机
Jigsaw 项目旨在为 Java SE 平台设计和实现一个标准模块系统,并将该系统应用于平台本身和 JDK。其主要目标是使平台的实现更容易扩展到小型设备,提高安全性和可维护性,提高应用程序性能,并为开发人员提供更好的大型编程工具。
重组源代码的动机包括:
-
让JDK开发者有机会熟悉系统的模块化结构;
-
通过在构建中强制执行模块边界来保留该结构,甚至在引入模块系统之前;和
-
使 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 包名称,其中句点替换为斜杠。
举一个简单的例子,存储库java.lang.Object
中类的源代码jdk
驻留在两个文件中,一个是 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
名称前添加下划线,所有这些都是为了简化纯 Java 模块的常见情况:native``conf
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 的范围。 -
有些模块有多个 本地库;我们是否应该合并它们,以便每个模块最多有一个本机库? - 不;在某些情况下,我们需要每个模块具有多个本机库的灵活性,例如“无头”与“有头”AWT。
测试
如前所述,此 JEP 不会更改 JRE 和 JDK 二进制映像的结构,仅对内容进行了较小的更改。因此,我们通过将使用它构建的图像与不使用它构建的图像进行比较来验证此更改,并运行测试来验证实际的微小更改。
风险和假设
我们假设 Mercurial 将能够处理实施此更改所需的大量文件重命名操作,并保留该过程中的所有历史信息。早期测试表明 Mercurial 能够做到这一点,但仍然存在一个较小的风险,即某些文件的新旧位置之间的关系未正确记录。在这种情况下,旧位置的文件历史记录仍将位于存储库中;只是会更难找到。
不可能将使用旧方案针对存储库创建的补丁直接应用到使用新方案的存储库,反之亦然。为了缓解这个问题,我们开发了一个脚本来将补丁中的文件名从旧位置翻译到新位置。
依赖关系
此 JEP 是Project Jigsaw的多个 JEP 中的第二个。它合并了JEP 200中 JDK 模块化结构的定义,但它并不明确依赖于该 JEP。