-
创建时间 2014/07/22 14:08
-
最近更新 2017/03/08 13:58
-
所属类型 功能
-
项目状态 已完成/已交付
-
域 实现
-
讨论 拼图的开发在 openjdk.java.net
-
努力级别 L
-
持续时间 L
-
优先级 1
检验人 Alan Bateman, Alex Buckley, Mandy Chung, Paul Sandoz
-
支持 Brian Goetz
-
发行 9
-
版本 8051619
-
块 JEP 200: The Modular JDK
-
相关 JEP 220: Modular Run-Time Images
概要
将 JDK 源代码重新组织为模块,对构建系统进行增强以使之支持对模块的编译,并在构建时强制对模块边界的应用。
非目标
此 JEP 不是为了改变 JRE 和 JDK 二进制镜像的结构,也不是为了将模块系统引入JDK。这一任务将交由相关 JEP 或恰当的 JSR 来完成。
此 JEP 将定义新的 JDK 源代码布局。该新的布局或许亦可被 JDK 之外的项目所采用,但此 JEP 的目标并不在于设计一个能被广泛接受的万能的模块化源代码布局。
动机
拼图项目着眼于为 Java SE 平台设计并实现一个标准的模块系统,并将这一系统应用到平台本身,以及 JDK 上。这一项目的主要目标是为了使平台的实现更易于扩展到更小的设备上,增强安全性与可维护性,加强应用的性能,以及为开发者在大规模的应用开发中提供更好的工具。
此 JEP 是拼图项目的第一步。随后的 JEP 将模块化 JRE 和 JDK 镜像 (JEP 220),并引入模块系统 (JEP 261)。
之所以把重新组织源代码这一任务放在较靠前的位置,是出于如下考虑:
- 让 JDK 开发者们有机会对系统的模块化架构进行熟悉。
- 从现在开始通过在构建时对模块边界的强制应用,保留模块结构。即使模块系统尚未引入。以及
- 使未来的拼图项目开发工作得以顺利开展,而无需不断地区考虑如何将当前的非模块化源代码进行模块化的问题。
描述
当前架构
现今 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
src/share/native/java/lang/Object.c
另一个稍微复杂一点的例子,包私有的类 java.lang.ProcessImpl
和 ProcessEnvironment
的源代码是特定操作系统相关的,对于 Unix 类系统,它存在于三个文件中:
src/solaris/classes/java/lang/ProcessImpl.java
src/solaris/classes/java/lang/ProcessEnvironment.java
src/solaris/native/java/lang/ProcessEnvironment_md.c
(没错,这些源代码适用于所有 Unix 衍生系统,但其二级目录却以 solaris 命名。稍后将会谈到这个问题)
在 src/{share,$OS}
目录下有少部分目录不符合当前的结构,这包括了:
目录 | 内容 |
---|---|
src/{share,$OS}/back | JDWP 后台 |
src/{share,$OS}/bin | Java 启动器 |
src/{share,$OS}/instrument | 仪器仪表支持 |
src/{share,$OS}/javavm | 到处的 JVM 包含文件 |
src/{share,OS}/lib | JAVA_HOME/lib 的文件 | |
src/{share,$OS}/transport | JDWP 传送 |
新架构
JDK 模块化项目为重新组织源代码,让其更易于维护提供了一个难得的机会。我们建议将以下新的结构应用于除 hotspot 外的所有 JDK 代码仓库:
src/$MODULE/{share,$OS}/classes/$PACKAGE/*.java
src/$MODULE/{share,$OS}/native/include/*.{h,hpp}
src/$MODULE/{share,$OS}/native/$LIBRARY/*.{c,cpp}
src/$MODULE/{share,$OS}/conf/*
- $MODULE 模块名(如 java.base)
- share 如之前的结构一样包含了共享的,跨平台的代码
- OS 目录如之前的结构一样存放特定操作系统相关代码,OS 可能是 solaris, windows 等取值之一
- classes 目录如之前一样包含了 Java 源代码和资源文件。他们以目录树的形式组织,体现了他们在 API $PACKAGE 中的继承关系
- native 目录如之前一样存放 C 和 C++ 源代码,但组织形式有所不同:
-
include 目录存放用于被导出供外部使用的 C 和 C++ 头文件(如
jni.h
) -
C 或 C++ 源文件放在 $LIBRARY 目录,此目录以共享库名字命名,或以编译后的代码将会被链接到的动态链接库命名(例如
libjava
或libawt
)conf 目录包含将被最终用户修改的配置文件(如
net.properties
)
-
现在让我们再看看在新的结构下,java.lang.Object
源文件会是怎样的结构:
src/java.base/share/classes/java/lang/Object.java
src/java.base/share/native/libjava/Object.c
而包私有类 java.lang.ProcessImpl
和 ProcessEnvironment
的源文件结构将变成这样:
src/java.base/unix/classes/java/lang/ProcessImpl.java
src/java.base/unix/classes/java/lang/ProcessEnvironment.java
src/java.base/unix/native/libjava/ProcessEnvironment_md.c
(我们抓住这个机会,终于把 solaris 目录名字改成了 unix)
当前在 src/{share, $OS}
目录下的不符合结构规范的内容将会被迁移到恰当的模块下:
目录 | 模块 |
---|---|
src/{share,$OS}/back | jdk.jdwp.agent |
src/{share,$OS}/bin | java.base |
src/{share,$OS}/instrument | java.instrument |
src/{share,$OS}/javavm | java.base |
src/{share,OS}/lib | MODULE/{share,$OS}/conf | |
src/{share,$OS}/transport | jdk.jdwp.agent |
当前在 lib
目录下的不应被最终用户修改的文件将会被转换成资源文件
构建系统的改变
构建系统将会被修改成每次编译一个模块,而不是像从前一样每次编译一个代码仓库。而将会以模块图的反向拓扑排序为顺序进行编译。与其他任何模块没有直接或间接一来的模块,将会尽可能地并行编译。
按照模块而非代码仓库为单位进行编译的一个额外好处是诸如 corba,jaxp 和 jaxws 这些代码仓库将可以使用 Java 语言的新功能和 API。在以前这是被禁止的,因为这些代码仓库都会在 JDK 仓库之前被编译。
一个中间版本(非镜像)的编译文件会被以模块进行归类存放。现在这些文件的结构是这样的:
jdk/classes/*.class
更新的构建系统则会产生如下目录结构:
jdk/modules/$MODULE/*.class
镜像的结构则不会被改变,他们的内容只会有小小的不同。只要有可能,构建系统就会在构建时强制检查模块边界。如果出现模块边界被破坏的情况,构建将会失败。模块边界的定义会在 modules.xml
中进行,modules.xml
会和源代码一起被维护。这个文件在JEP 200中有进行说明。任何对此文件的更改都需要由拼图项目的提交者进行审核。
其他方案
除以上提到的方案外,还有很多其他可能的源代码架构,包括:
-
将 {share, OS} 维持在顶层,然后引入一个模块目录来存放模块的 class 文件:`src/{share,OS}/modules/MODULE/PACKAGE/*.java`
src/{share,$OS}/native/include/*.{h,hpp}
src/{share,$OS}/native/$LIBRARY/*.{c,cpp}
src/{share,$OS}/conf/*
-
把所有内容放到恰当的 MODULE 目录下, 但将 {share, OS} 维持在顶端:
src/{share,$OS}/$MODULE/classes/$PACKAGE/*.java
src/{share,$OS}/$MODULE/native/include/*.{h,hpp}
src/{share,$OS}/$MODULE/native/$LIBRARY/*.{c,cpp}
src/{share,$OS}/$MODULE/conf/*
-
如当前方案建议的一样把 {share, OS} 移到 MODULE 目录下,但移除中间类目录,并给 native 和 conf 目录加下划线前缀,以简化纯 Java 模块的通用结构。
src/$MODULE/{share,$OS}/$PACKAGE/*.java
src/$MODULE/{share,$OS}/_native/include/*.{h,hpp}
src/$MODULE/{share,$OS}/_native/$LIBRARY/*.{c,cpp}
src/$MODULE/{share,$OS}/_conf/*
-
方案 3 的变体,但 {share, OS} 在顶层:`src/{share,OS}/MODULE/PACKAGE/*.java`
src/{share,$OS}/$MODULE/_native/include/*.{h,hpp}
src/{share,$OS}/$MODULE/_native/$LIBRARY/*.{c,cpp}
src/{share,$OS}/$MODULE/_conf/*
-
另一个方案 3 的变体,将 {share, OS} 移到目录树更深处,以简化没有特定操作系统代码的纯 Java 模块的目录结构:`src/MODULE/$PACKAGE/*.java`
src/$MODULE/_native/include/*.{h,hpp}
src/$MODULE/_native/$LIBRARY/*.{c,cpp}
src/$MODULE/_conf/*
src/$MODULE/_$OS/$PACKAGE/*.java
src/$MODULE/_$OS/_native/include/*.{h,hpp}
src/$MODULE/_$OS/_native/$LIBRARY/*.{c,cpp}
src/$MODULE/_$OS/_conf/*
因为方案 3~5 在目录结构中采用了下划线,这会导致目录名很异样,并且难以浏览,我们拒绝了这几个方案。相比方案 1 和 2 我们更偏爱现在这个方案,因为现在这个方案把同一模块的源代码放到一个目录下,这意味着从当前架构迁移过来需要的改动更少。依赖于当前结构的工具和脚本需要修改,但至少对于 Java 源代码来说在每个 $MODULE 目录之下的结构与之前并没有变化。
另外我们还考虑了以下的问题:
- 我们是否应该定义独立的资源文件目录,以将它们和 Java 源文件分割开来? — 不,看起来并不值得这么做。
- 某些模块具有横跨多个代码仓库的内容。这是不是个问题? — 这是个麻烦,但构建系统可以利用 VPATH 机制的魔力应付这一情况。随着时间的推移我们也许会重新架构代码仓库,减少甚至消除跨仓库的模块,但那已经超出本 JEP 的讨论范围。
- 某些模块含有多个本地调用库。我们是否应该做一些合并,是的每个模块只有最多一个本地调用库? — 不,在某些情况下,我们需要一个模块下含有多个本地调用库所带来的灵活性。
测试
如前所述,此 JEP 并不会更改 JRE 和 JDK 二进制镜像的结构,对内容的改变也少之又少。因此我们可以通过对比包含改变与不包含改变的两个镜像来进行验证,并针对那些少量的改变跑一些单元测试。
风险与假设
我们假设 Mecurial 能够应付实现这一更改所需要进行的大量文件重命名操作,并在这一过程中将所有历史信息保留下来。早期的测试证明了 Mercurial 可以胜任这一工作,但在新旧文件的位置关系是否能够正确地被记录下来这一问题上仍然有一些小小的风险。如果真出现了这种情况,出问题的文件的历史记录仍会保存在代码仓库中,只是会很难找出来。
将一个针对使用旧目录结构的代码仓库的补丁使用到使用新目录结构的代码仓库上,或反过来,会变成不可能的事。为缓解这个问题我们打算开发一个可以将补丁中旧文件地址自动转换成新地址的脚本。
依赖关系
本 JEP 是拼图项目所有相关 JEP 中的第二份。它基于在 JEP 200 中所定义的 JDK 模块化结构进行编写,但它并不显示地依赖于 JEP 200。