当Jigsaw在Java 9中最终发布时,这个项目的历史已经超过八年了。 在最初的几年中,它必须要与另外两个类似的Java规范请求(Java Specification Request)进行竞争,这两个规范名为JSR 277 Java模块系统(Java Module System)以及JSR 294 增强的模块化支持(Improved Modularity Support)。它还导致了与OSGi社区的冲突,人们担心Jigsaw项目会成为不必要且不完备的功能性重复,逼迫Java开发人员必须在两种互不兼容的模块系统中做出选择。 在早期,这个项目并没有充足的人手,在2010年Sun并入Oracle的时候,甚至一度中断。直到2011年,在Java中需要模块系统的强烈需求被重申,这项工作才得到完全恢复。 接下来的三年是一个探索的阶段,结束于2014年的7月,当时建立了多项Java增强提议( Java Enhancement Proposal),包括JEP 200 模块化JDK(Modular JDK)、JEP 201 模块化源码(Modular Source Code)和JEP 220 模块化运行时镜像(Modular Run-Time Image),以及最终的JSR 376 Java平台模块系统(Java Platform Module System)。上述的最后一项定义了真正的Java模块系统,它将会在JDK中以一个新JEP的形式来实现。 在2015年7月,JDK划分为哪些模块已经大致确定(参见JEP 200),JDK的源码也进行了重构来适应这种变化(参见JEP 201),运行时镜像(run-time image)也为模块化做好了准备(参见JEP 220)。所有的这些都可以在当前JDK 9的预览版中看到。 针对JSR 376所开发的代码很快将会部署到JDK仓库中,但是令人遗憾的是,现在模块化系统本身尚无法体验。(目前,Java 9的预览版本已经包含了模块化功能。——译者注) 驱动力在Jigsaw项目的历史中,它的驱动力也发生过一些变化。最初,它只是想模块化JDK。但是当人们意识到如果能够在库和应用程序的代码中也使用该工具的话,将会带来非常大的收益,于是它的范围得到了扩展。 不断增长且不可分割的Java运行时Java运行时的大小在不断地增长。但是在Java 8之前,我们并没有办法安装JRE的子集。所有的Java安装包中都会包含各种库的分发版本,如XML、SQL以及Swing的API,不管我们是否需要它们,都要将其包含进来。 对 于中等规模(如桌面PC和笔记本电脑)以上的计算设备来说,这不算是什么严重的问题,但是对于小型的设备来说,这就很严重了,比如在路由器、TV盒子和汽 车上,还有其他使用Java的小地方。随着当前容器化的趋势,在服务器领域也有相关的要求,因为减少镜像的大小就意味着降低成本。 Java 8引入了 compact profile的功能,它们定义了三个Java SE的子集。在一定程度上缓解了这个问题,但是它们只有在严格限制的场景下才能发挥作用,profile过于死板,无法涵盖现在和未来所有使用JRE部分功能的需求。 JAR/Classpath地狱JAR地狱和Classpath地狱是一种诙谐的说法,指的是Java类加载机制的缺陷所引发的问题。尤其是在大型的应用中,它们可能会以各种方式产生令人痛苦的问题。有一些问题是因为其他的问题而引发的,而有一些则是独立的。 无法表述依赖JAR文件无法以一种JVM能够理解的方式来表述它依赖于哪些其他的JAR。因此,就需要用户手动识别并满足这些依赖,这要求用户阅读文档、找到正确的项目、下载JAR文件并将其添加到项目中。 而且,有一些依赖是可选的,只有用户在使用特定功能的特性时,某个JAR才会依赖另外一个JAR。这会使得这个过程更加复杂。 Java运行时在实际使用某项依赖之前,并不能探测到这个依赖是无法满足的。如果出现这种情况,将会出现 像Maven这样的构建工具能够帮助解决这个问题。 传递性依赖一个应用程序要运行起来可能只需依赖几个库就足够了,但是这些库又会需要一些其他的库。问题组合起来会变得更加复杂,在所消耗的体力以及出错的可能性上,它会呈指数级地增长。 同样,构建工具能够在这个问题上提供一些帮助。 遮蔽有时候,在classpath的不同JAR包中可能会包含全限定名完全相同的类,比如我们使用同一个库的两个不同版本。因为类会从classpath中的第一个JAR包中加载,所以这个版本的变种将会“遮蔽”所有其他的版本,使它们变得不可用。 如 果这些不同的变种在语义上有所差别,那将会导致各种级别的问题,从难以发现的不正常行为到非常严重的错误都是有可能的。更糟糕的是,问题的表现形式是不确 定的。这取决于JAR文件在classpath中的顺序。在不同的环境下,可能也会有所区别,例如开发人员的IDE与代码最终运行的生产机器之间就可能有 所差别。 版本冲突如果项目中有两个所需的库依赖不同版本的第三个库,那么将会产生这个问题。 如 果这个库的两个版本都添加到classpath中的话,那么最终的行为是不可预知的。首先,因为前面所述的遮蔽问题,两个版本的类中,只会有一个能够加载 进来。更糟糕的是,如果某个类位于一个JAR包中,但是它所访问的其他类却不在这个包中,这个类也能够加载。所导致的结果就是,对这个库的代码调用将会混 合在两个版本之中。 在最好的情况下,如果试图访问所加载的类中不存在的代码,将会导致明显的 识别这种情况所导致的难以预料的行为是很困难的,也无法直接解决。 复杂的类加载机制默认情况下,所有的类由同一个 这很快就会导致复杂的类加载机制,从而产生难以预期和难以理解的行为。 在包之间,只有很弱的封装机制如果类位于同一个包中,那Java的可见性修饰符提供了一种很棒的方式来实现这些类之间的封装。但是,要跨越包之间边界的话,那只能使用一种可见性:public。 因为类加载器会将所有加载进来的包放在一起,public的类对其他所有的类都是可见的,因此,如果我们想创建一项功能,这项功能对某个JAR是可用的,而对于这个JAR之外是不可用的,这是没有办法实现的。 手动的安全性包之间弱封装性所带来的一个直接结果就是,安全相关的功能将会暴露在同一个环境中的所有代码面前。这意味着,恶意代码有可能绕过安全限制,访问关键的功能。 从Java 1.1开始,有一种hack的方式,能够防止这种状况:每当进入安全相关的代码路径时,将会调用 启动性能最后,Java运行时加载并JIT编译全部所需的类需要较长的时间。其中一个原因在于类加载机制会对classpath下的所有JAR执行线性的扫描。类似的,在识别某个注解的使用情况时,需要探查classpath下所有的类。 目标Jigsaw项目的目标就是解决上面所述的问题,它会引入一个语言级别的机制,用来模块化大型的系统。这种机制将会用在JDK本身中,开发人员也可以将其用于自己的项目之中。 需 要注意的是,对于JDK和我们开发人员来说,并不是所有的目标都具有相同的重要性。有一些与JDK具有更强的相关性,并且大多数都对日常的编程不会带来巨 大的影响(这与最近的语言修改形成了对比,如lambda表达式和默认方法)。不过,它们依然会改变大型项目的开发和部署。 可扩展性的平台JDK在模块化之后,用户就能挑出他们需要的功能,并创建自己的JRE,在这个JRE中只包含他们需要的模块。这有助于在小型设备和容器领域中,保持Java作为关键参与者的地位。
可靠的配置通过这个规范,某个模块能够声明对其他模块的依赖。运行时环境能够在编译期(compile-time)、构建期(build-time)以及启动期(launch-time)分析这些依赖,如果缺少依赖或依赖冲突的话,很快就会发生失败。 强封装Jigsaw项目的一个主要目标就是让模块只导出特定的包,其他的包是模块私有的。
提升安全性和可维护性在模块中,内部API的强封装会极大地提升安全性,因为核心代码对于没有必要使用它们的其余代码来讲是隐藏起来的。维护也会变得更加容易,这是因为我们能够更容易地将模块的公开API变得更小。
提升性能因为能够更加清晰地界定所使用代码的边界,现有的优化技术能够更加高效地运用。
核心概念因为模块化是目标,所以Jigsaw项目引入了模块(module)的概念,描述如下:
为了能够基于一定的上下文环境来了解模块,我们可以想一下知名的库,如Google Guava或Apache Commons中的库(比如Collections或IO),将其作为模块。根据作者希望划分的粒度,每个库都可能划分为多个模块。 对于应用来说也是如此。它可以作为一个单体(monolithic)的模块,也可以进行拆分。在确定如何将其划分为模块时,项目的规模和内聚性将是重要的因素。 按照规划,在组织代码时,模块将会成为开发人员工具箱中的常规工具。
模块又可以进一步组合为开发阶段的各种配置,这些阶段也就是编译期、构建期、安装期以及运行期。对于我们这样的Java用户来说,可以这样做(在这种情况下,通常会将其称之为开发者模块),同时这种方式还可以用来剖析Java运行时本身(此时,它们通常称之为平台模块)。 实际上,这就是JDK目前进行模块化的规划。 (点击放大图像) 特性那么,模块是如何运行的呢?查阅一下Jigsaw项目的需求以及 JSR 376将会帮助我们对其有所了解。 依赖管理为了解决“JAR/Classpath地狱”的问题,Jigsaw项目的一个关键特性就是依赖管理。让我们看一下这些相关的组件。 声明与解析模块将会声明它需要哪些其他的模块才能编译和运行。模块系统会使用该信息传递性地识别所有需要的模块,从而保证初始的那个模块能够编译和运行。 我们还可以不依赖具体的模块,而是依赖一组接口。模块系统将会试图据此识别模块,这些模块实现了所依赖的接口,能够满足依赖,系统会将其绑定到对应的接口中。 版本化模块将会进行版本化。它们能够标记自己的版本(在很大程度上可以是任意格式,只要能够完全表示顺序就行),版本还能用于限制依赖。在任意阶段都能覆盖这两部分信息。模块系统会在各个阶段都强制要求配置能够满足所有的限制。 Jigsaw项目不一定会支持在一个配置中存在某个模块的多个版本。但是,稍等,那该如何解决JAR地狱的问题呢? 好问题! 版 本选择——针对同一个模块,在一组不同版本中挑选最合适的版本——并没有作为规范所要完成的任务。所以,在我撰写的上文中,模块系统会识别所需的模块进行 编译,在运行时则可能会使用另外一个模块,这都基于一个假设,那就是环境中只存在模块的一个版本。如果存在多个版本的话,那么上游的步骤(如开发人员或者 他所使用的构建工具)必须要做出选择,系统只会校验它能满足所有的约束。 封装模块系统会在各个阶段强制要求强封装。这是围绕着一个导出机制实现的,在这种情况下,只有模块导出的包才能访问。封装与 这个提议的具体语法尚没有定义,但是JEP 200提供了一些关键语义的XML实例。作为样例,如下的代码声明了
从这个代码片段我们可以看出,java.sql依赖于 导出模块会声明特定的包进行导出,只有包含在这些包中的类型才能导出。这意味着其他模块只能看到和使用这些类型。更严格是,其他模块必须要显式声明依赖包含这些类型的模块,这些类型才能导出到对应的模块中。 非常有意思的是,不同的模块能够包含相同名称的包,这些模块甚至还能够将其导出。 在上面的样例中, 重新导出我们还能够在某个模块中重新导出它所依赖的模块中的API(或者是其中的一部分)。这将会对重构提供支持,我们能够在不破坏依赖的情况下拆分或合并模块,因为最初的依赖可以继续存在。重构后的模块可以导出与之前相同的包,即便它们可能不会包含所有的代码。在极端的情况下,有一种所谓的聚合器模块(aggregator module),它可以根本不包含任何代码,只是作为一组模块的抽象。实际上,Java 8中所提供的compact profile就是这样做的。 从上面的例子中,我们可以看到 限制导出为了帮助开发者(尤其是模块化JDK的人员)让他们所导出API的有较小的接触面,有一种可选的限制导出(qualified export)机制,它允许某个模块将一些包声明为只针对一组特定的模块进行导出。所以使用“标准”机制时,导出功能的模块并不知道(也不关心)谁会访问这些包,但是通过限制导出机制,能够让一个模块限定可能产生的依赖。 配置、阶段以及保真性(Fidelity)如前所述,JEP 200的目标之一就 是模块能够在开发的各个阶段组合为各种配置。对于平台模块可以如此,这样就能够创建与完整JRE或JDK类似的镜像,Java 8所引入的compact profile以及包含特定模块集合(及其级联依赖)的任意自定义配置都使用了这种机制。类似的,开发人员也可以使用这种机制来组合他们应用程序的不同变 种。 在编译期(compile-time),要编译的代码只能看到所配置的模块集合中导出的包。在构建期(build-time),借助一个新的工具(可能会被称为JLink),我们能够创建只包含特定模块及其依赖的二进制运行时镜像。在安装期(launch-time),镜像能够看起来就像是只包含了它所具有的模块的一个子集。 我们还能够替换实现了授权标准(endorsed standard)和 独立技术(standalone technology)的模块,在任意的阶段都能将其替换为较新的版本。这将会替代已废弃的授权标准重载机制(endorsed standards override mechanism)以及扩展机制(参见下文。) 模块系统的各个方面(如依赖管理、封装等等),在所有阶段的运行方式是完全相同的,除非因为特定的原因,在某些阶段无法实现。 模块相关的所有信息(如版本、依赖以及包导出)都会在代码文件中进行描述,这样会独立于IDE和构建工具。 性能全程序优化的技术在模块系统中,借助强封装技术,能够很容易自动计算出一段特定的代码都用在了哪些地方。这会使得程序分析和优化技术更加可行:
有一些被称为全程序优化(whole-program optimization)的技术,在Java 9中至少会实现两种这样的技术。还有包含一个工具,使用这个工具能够分析给定的一组模块,并使用上述的优化技术,创建更加高性能的二进制镜像。 注解目前,要自动发现带有注解的类(如Spring注解标注的配置类),需要扫描特定包下的所有类。这通常会在程序启动的时候完成,这在相当程度上会减慢启动的过程。 模块将会提供一个API,允许调用者识别所有带有给定注解的类。一种预期的方式是为这样的类创建索引,这个索引会在模块编译的时候创建。 与已有的概念和工具集成诊断工具(如栈跟踪信息)将会进行更新,其中会包含模块的信息。而且,它们还会集成到反射API中,这样就能按照操作类的方式来使用它们,还会包含版本信息,这一信息可以进行反射,也可以在运行时重载。 模块的设计能够让我们在使用构建工具时“尽可能地减少麻烦(with a minimum of fuss)”。编译之后的模块能够用在classpath中,也能作为一个模块来使用,这样的话,库的开发人员就没有必要为classpath应用和基于模块的应用分别创建多个构件了。 与其他模块系统的相互操作也进行了规划,这其中最著名的也就是OSGi。 尽管模块能够对其他的模块隐藏包,但是我们依然能够对模块包含的类和接口执行白盒测试。 特定OS的包模块系统在设计时,始终考虑到了包管理器文件格式,“如RPM、Debian以及Solaris IPS”。开发人员不仅能够使用已有的工具将一组模块集合创建为特定OS的包,这些模块还能调用按照相同机制安装的其他模块。 开发人员还能够将组成应用的一组模块打包为特定OS的包,“终端用户能够按照目标系统的通用做法,安装和调用所打成的包”。基于上述的介绍,我们可以得知只有目标系统中不存在的模块才必须要打包进来。 动态配置正在运行中的应用能够创建、运行并发布独立的模块配置。在这些配置中,可以包含开发者和平台模块。对于容器类架构,这会非常有用,如IDE、应用服务器或其他Java EE平台。 不兼容性按照Java的惯例,这些变更在实现时,会强烈关注到向后的兼容性,所有标准和非废弃的API及机制都能够继续使用。但是项目可能会依赖其他缺乏文档的构造,这样的话,在往Java 9迁移的时候,就需要一些额外的工作了。 内部API不可用了借助于强封装,每个模块能够明确声明哪些类型会作为其API的一部分。JDK将会使用这个特性来封装所有的内部API,因此它们会变得不可用了。 在Java 9所带来的不兼容性中,这可能是涵盖范围最大的一部分。但是这也是最明显的,因为它会导致编译错误。 那么,什么是内部API呢?毫无疑问,位于 能产生特殊问题的一个样例就是 另外一个样例是 合并JDK和JRE在具有可扩展的Java运行时之后,它允许我们很灵活地创建运行时镜像,JDK和JRE就丧失了其独有的特性,它们只是模块组合中的两种形式而已。 这意味着,这两个构件将会具有相同的结构,包括目录结构也相同,任何依赖它(如在原来的JDK目录中会有名为jre的子目录)的代码就不能正常运行了。 内部JAR不可用了像 任何假设这些文件存在的代码将无法正确运行。这可能对IDE或其他严重依赖这些文件的工具带来一些切换的麻烦。 针对运行时镜像内容的新URL模式在运行时,有些API会返回针对类和资源文件的URL(如 ClassLoader.getSystemResource)。在Java 9之前,它们都是jar URL,格式如下:
Jigsaw项目将会使用模块作为代码文件的容器,单个JAR将不可用了。这需要一个新的格式,所以这些API将会返回jrt URL:
如果使用这些API所返回的实例来访问文件的代码(如 URL.getContent),那么运行方式会和现在一样。但是,如果依赖于jar URL的结构(比如手动构建它们或对其进行解析),那么就会出错了。 移除授权标准重载机制有一些Java API被称为“独立技术(Standalone Technology)”,它们的创建是在Java Community Process(如 JAXB)之外的。对它们来说,有可能会升级其依赖或使用替代实现。授权标准重载机制允许在JDK中安装这些标准的替代版本。 这种机制在Java 8中已经废弃了,在Java 9中将会移除,会由上文所述的可升级模块来替代。 移除扩展机制借助扩展机制,自定义API能够被JDK中运行的所有应用程序使用,而不必在classpath中对其进行命名。 这种机制在Java 8中已经废弃了,在Java 9中将会移除。有些本身有用的特性将会保留。 接下来要做什么?我们已经简要了解了Jigsaw项目的历史,看到是什么在驱动它的发展并讨论了它的目标,如何通过一些特性来实现这些目标。除了等待Java 9以外,我们还能做些什么呢? 准备我们应该为自己的项目做一些准备工作,检查它们是否依赖Java 9中将要移除的内容。 至少,在检查内部API依赖方面不再需要手动搜索了。从Java 8开始,JDK包含了Java依赖分析工具(Java Dependency Analysis Tool),名为JDeps (介绍了一些内部的包,官方有针对Windows以及 Unix的文档),它能够列出某个项目依赖的所有的包。如果在运行时使用- 之所以说“几乎所有”是因为它还无法识别Java 9中不可用的所有的包。这至少会影响到JavaFX所属的包,可以查看 JDK-8077349。(通过使用这个搜索,除了缺失的功能以外,我没能发现其他的缺陷。) 至少存在三个用于Maven的JDeps插件:分别由Apache、Philippe Marschall以及我本人所提供。就目前来讲,最后一个是唯一当 讨论Jigsaw项目最新的消息来源于Jigsaw-Dev邮件列表。我也会在博客中继续讨论这个话题。 如果你担心某个特定的API在Java 9中不可用的话,那么你可以查看相关OpenJDK项目的邮件列表,因为他们会负责开发公开的版本。 采用Java 9的早期试用构建版本已经可用了。不过,JSR 376依然处于开发阶段,在这些构建版本中尚无法使用模块系统,还会有很多的变化。(在目前的试用版中,已经包含了Jigsaw,不过最新的消息是Java 9又要延期六个月发布了。——译者注)实际上,除了强封装以外,其他的功能都已经就绪了。 将收集到的消息发送给Jigsaw-Dev邮件列表能够反馈给项目。最后,引用JEP 220(临近)结尾的一段话: 我们不可能抽象地确定这些变更的全部影响,所以必须要依赖广泛的内部测试,尤其重要的还有外部测试。[……]如果有些变更会给开发人员、部署人员或终端用户带来难以承受的负担,那么我们将会研究减少其影响的方式。 另外,还有一个全球的Java用户群组AdoptOpenJDK,它能够很好地将早期试用者联系起来。 关于作者
|