当Jigsaw在Java 9中最终发布时,这个项目的历史已经超过八年了。html
转载于:http://www.itxuexiwang.com/a/liunxjishu/2016/0228/180.html?1456925937java
在早期,这个项目并无充足的人手,在2010年Sun并入Oracle的时候,甚至一度中断。直到2011年,在Java中须要模块系统的强烈需求被重申,这项工做才获得彻底恢复。linux
接下来的三年是一个探索的阶段,结束于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的形式来实现。sql
在2015年7月,JDK划分为哪些模块已经大体肯定(参见JEP 200),JDK的源码也进行了重构来适应这种变化(参见JEP 201),运行时镜像(run-time image)也为模块化作好了准备(参见JEP 220)。全部的这些均可以在当前JDK 9的预览版中看到。编程
针对JSR 376所开发的代码很快将会部署到JDK仓库中,可是使人遗憾的是,如今模块化系统自己尚没法体验。(目前,Java 9的预览版本已经包含了模块化功能。——译者注)安全
在Jigsaw项目的历史中,它的驱动力也发生过一些变化。最初,它只是想模块化JDK。可是当人们意识到若是可以在库和应用程序的代码中也使用该工具的话,将会带来很是大的收益,因而它的范围获得了扩展。#p#分页标题#e#服务器
Java运行时的大小在不断地增加。可是在Java 8以前,咱们并无办法安装JRE的子集。全部的Java安装包中都会包含各类库的分发版本,如XML、SQL以及Swing的API,无论咱们是否须要它们,都要将其包含进来。架构
对于中等规模(如桌面PC和笔记本电脑)以上的计算设备来讲,这不算是什么严重的问题,可是对于小型的设备来讲,这就很严重了,好比在路由器、TV盒子和汽车上,还有其余使用Java的小地方。随着当前容器化的趋势,在服务器领域也有相关的要求,由于减小镜像的大小就意味着下降成本。并发
Java 8引入了compact profile的功能,它们定义了三个Java SE的子集。在必定程度上缓解了这个问题,可是它们只有在严格限制的场景下才能发挥做用,profile过于死板,没法涵盖如今和将来全部使用JRE部分功能的需求。ide
JAR地狱和Classpath地狱是一种诙谐的说法,指的是Java类加载机制的缺陷所引起的问题。尤为是在大型的应用中,它们可能会以各类方式产生使人痛苦的问题。有一些问题是由于其余的问题而引起的,而有一些则是独立的。
JAR文件没法以一种JVM可以理解的方式来表述它依赖于哪些其余的JAR。所以,就须要用户手动识别并知足这些依赖,这要求用户阅读文档、找到正确的项目、下载JAR文件并将其添加到项目中。
并且,有一些依赖是可选的,只有用户在使用特定功能的特性时,某个JAR才会依赖另一个JAR。这会使得这个过程更加复杂。#p#分页标题#e#
Java运行时在实际使用某项依赖以前,并不能探测到这个依赖是没法知足的。若是出现这种状况,将会出现NoClassDefFoundError
异常,进而致使正在运行的应用崩溃。
像Maven这样的构建工具可以帮助解决这个问题。
一个应用程序要运行起来可能只需依赖几个库就足够了,可是这些库又会须要一些其余的库。问题组合起来会变得更加复杂,在所消耗的体力以及出错的可能性上,它会呈指数级地增加。
一样,构建工具可以在这个问题上提供一些帮助。
有时候,在classpath的不一样JAR包中可能会包含全限定名彻底相同的类,好比咱们使用同一个库的两个不一样版本。由于类会从classpath中的第一个JAR包中加载,因此这个版本的变种将会“遮蔽”全部其余的版本,使它们变得不可用。
若是这些不一样的变种在语义上有所差异,那将会致使各类级别的问题,从难以发现的不正常行为到很是严重的错误都是有可能的。更糟糕的是,问题的表现形式是不肯定的。这取决于JAR文件在classpath中的顺序。在不一样的环境下,可能也会有所区别,例如开发人员的IDE与代码最终运行的生产机器之间就可能有所差异。
若是项目中有两个所需的库依赖不一样版本的第三个库,那么将会产生这个问题。#p#分页标题#e#
若是这个库的两个版本都添加到classpath中的话,那么最终的行为是不可预知的。首先,由于前面所述的遮蔽问题,两个版本的类中,只会有一个可以加载进来。更糟糕的是,若是某个类位于一个JAR包中,可是它所访问的其余类却不在这个包中,这个类也可以加载。所致使的结果就是,对这个库的代码调用将会混合在两个版本之中。
在最好的状况下,若是试图访问所加载的类中不存在的代码,将会致使明显的NoClassDefFoundError
错误。可是在最坏的状况下,版本之间的差异仅仅是在语义上,实际的行为会有细微的差异,这会引入很难发现的bug。
识别这种状况所致使的难以预料的行为是很困难的,也没法直接解决。
默认状况下,全部的类由同一个ClassLoader
负责加载,在有些场景下,可能有必要引入额外的加载器,例如容许用户加载新的类,对应用程序进行扩展。
这很快就会致使复杂的类加载机制,从而产生难以预期和难以理解的行为。
若是类位于同一个包中,那Java的可见性修饰符提供了一种很棒的方式来实现这些类之间的封装。可是,要跨越包之间边界的话,那只能使用一种可见性:public。
由于类加载器会将全部加载进来的包放在一块儿,public的类对其余全部的类都是可见的,所以,若是咱们想建立一项功能,这项功能对某个JAR是可用的,而对于这个JAR以外是不可用的,这是没有办法实现的。#p#分页标题#e#
包之间弱封装性所带来的一个直接结果就是,安全相关的功能将会暴露在同一个环境中的全部代码面前。这意味着,恶意代码有可能绕过安全限制,访问关键的功能。
从Java 1.1开始,有一种hack的方式,可以防止这种情况:每当进入安全相关的代码路径时,将会调用SecurityManager
,并判断是否是容许访问。更精确地讲,它应该在每一个这样的路径上都进行调用。过去,在有些地方遗漏了对它们的调用,从而出现了一些漏洞,这给Java带来了困扰。
最后,Java运行时加载并JIT编译所有所需的类须要较长的时间。其中一个缘由在于类加载机制会对classpath下的全部JAR执行线性的扫描。相似的,在识别某个注解的使用状况时,须要探查classpath下全部的类。
Jigsaw项目的目标就是解决上面所述的问题,它会引入一个语言级别的机制,用来模块化大型的系统。这种机制将会用在JDK自己中,开发人员也能够将其用于本身的项目之中。
须要注意的是,对于JDK和咱们开发人员来讲,并非全部的目标都具备相同的重要性。有一些与JDK具备更强的相关性,而且大多数都对平常的编程不会带来巨大的影响(这与最近的语言修改造成了对比,如lambda表达式和默认方法)。不过,它们依然会改变大型项目的开发和部署。
JDK在模块化以后,用户就能挑出他们须要的功能,并建立本身的JRE,在这个JRE中只包含他们须要的模块。这有助于在小型设备和容器领域中,保持Java做为关键参与者的地位。#p#分页标题#e#
在这个提议的规范中,容许将Java SE平台及其实现分解为一组组件,开发人员能够把这些组件组装起来,造成自定义的配置,里面只包含应用实际须要的功能。—— JSR 376
经过这个规范,某个模块可以声明对其余模块的依赖。运行时环境可以在编译期(compile-time)、构建期(build-time)以及启动期(launch-time)分析这些依赖,若是缺乏依赖或依赖冲突的话,很快就会发生失败。
Jigsaw项目的一个主要目标就是让模块只导出特定的包,其余的包是模块私有的。
模块中的私有类就像是类中的私有域。换句话说,模块的边界不只肯定了类和接口的可见性,还定义了它的可访问性。——Mark Reinhold所撰写的文章“Project Jigsaw:将宏伟蓝图转换为可聚焦的点”
在模块中,内部API的强封装会极大地提高安全性,由于核心代���对于没有必要使用它们的其他代码来说是隐藏起来的。维护也会变得更加容易,这是由于咱们可以更容易地将模块的公开API变得更小。
随意使用Java SE平台实现的内部API不只有安全风险,并且也会带来维护的负担。该提议规范可以提供强封装性,这样实现Java SE平台的组件就能阻止对其内部API的访问。 ——JSR 376#p#分页标题#e#
由于可以更加清晰地界定所使用代码的边界,现有的优化技术可以更加高效地运用。
不少预先(ahead-of-time)优化和全程序(whole-program)优化的技术会更加高效,由于可以得知某个类只会引用几个特定组件中的类,它并不能引用运行时所加载的任意类。 —— JSR 376
由于模块化是目标,因此Jigsaw项目引入了模块(module)的概念,描述以下:
命名、自描述的程序组件,会包含代码和数据。模块必须可以包含Java类和接口,组织为包的形式,同时也能以动态加载库的形式(dynamically-loadable library)包含原生代码。模块的数据必须可以包含静态资源文件和用户可编辑的配置文件。 —— Java平台模块系统:需求(草案2)
为了可以基于必定的上下文环境来了解模块,咱们能够想一下知名的库,如Google Guava或Apache Commons中的库(好比Collections或IO),将其做为模块。根据做者但愿划分的粒度,每一个库均可能划分为多个模块。
对于应用来讲也是如此。它能够做为一个单体(monolithic)的模块,也能够进行拆分。在肯定如何将其划分为模块时,项目的规模和内聚性将是重要的因素。
按照规划,在组织代码时,模块将会成为开发人员工具箱中的常规工具。
#p#分页标题#e#开发人员目前已经可以考虑到一些标准的程序组件,如语言层面的类和接口。模块将会是另一种程序组件,像类和接口同样,它们将会在程序开发的各个阶段发挥做用。 ——Mark Reinhold的文章“Project Jigsaw:将宏伟蓝图转换为可聚焦的点”
模块又能够进一步组合为开发阶段的各类配置,这些阶段也就是编译期、构建期、安装期以及运行期。对于咱们这样的Java用户来讲,能够这样作(在这种状况下,一般会将其称之为开发者模块),同时这种方式还能够用来剖析Java运行时自己(此时,它们一般称之为平台模块)。
实际上,这就是JDK目前进行模块化的规划。
(点击放大图像)
那么,模块是如何运行的呢?查阅一下Jigsaw项目的需求以及JSR 376将会帮助咱们对其有所了解。
为了解决“JAR/Classpath地狱”的问题,Jigsaw项目的一个关键特性就是依赖管理。让咱们看一下这些相关的组件。
模块将会声明它须要哪些其余的模块才能编译和运行。模块系统会使用该信息传递性地识别全部须要的模块,从而保证初始的那个模块可以编译和运行。#p#分页标题#e#
咱们还能够不依赖具体的模块,而是依赖一组接口。模块系统将会试图据此识别模块,这些模块实现了所依赖的接口,可以知足依赖,系统会将其绑定到对应的接口中。
模块将会进行版本化。它们可以标记本身的版本(在很大程度上能够是任意格式,只要可以彻底表示顺序就行),版本还能用于限制依赖。在任意阶段都能覆盖这两部分信息。模块系统会在各个阶段都强制要求配置可以知足全部的限制。
Jigsaw项目不必定会支持在一个配置中存在某个模块的多个版本。可是,稍等,那该如何解决JAR地狱的问题呢?好问题!
版本选择——针对同一个模块,在一组不一样版本中挑选最合适的版本——并无做为规范所要完成的任务。因此,在我撰写的上文中,模块系统会识别所需的模块进行编译,在运行时则可能会使用另一个模块,这都基于一个假设,那就是环境中只存在模块的一个版本。若是存在多个版本的话,那么上游的步骤(如开发人员或者他所使用的构建工具)必需要作出选择,系统只会校验它能知足全部的约束。
模块系统会在各个阶段强制要求强封装。这是围绕着一个导出机制实现的,在这种状况下,只有模块导出的包才能访问。封装与SecurityManager
所执行的安全检查是相独立的。
这个提议的具体语法尚没有定义,可是JEP 200提供了一些关键语义的XML实例。做为样例,以下的代码声明了java.sql
模块。
#p#分页标题#e#<module> <!-- 模块的名字 --> <name>java.sql</name> <!-- 每一个模块都会依赖java.base --> <depend>java.base</depend> <!-- 这个模块依赖于java.logging和java.xml 模块,并从新导出这些模块所导出的API包 --> <depend re-exports="true">java.logging</depend> <depend re-exports="true">java.xml</depend> <!-- 这个模块导出java.sql、javax.sql以及 javax.transaction.xa包给其余任意的模块 --> <export><name>java.sql</name></export> <export><name>javax.sql</name></export> <export><name>javax.transaction.xa</name></export> </module>
从这个代码片断咱们能够看出,java.sql依赖于java.base
、java.logging
以及java.xml
。在稍后介绍不一样的导出机制时,咱们就能理解上文中其余的声明了。
模块会声明特定的包进行导出,只有包含在这些包中的类型才能导出。这意味着其余模块只能看到和使用这些类型。更严格是,其余模块必需要显式声明依赖包含这些类型的模块,这些类型才能导出到对应的模块中。
很是有意思的是,不一样的模块可以包含相同名称的包,这些模块甚至还可以将其导出。
在上面的样例中,java.sql
导出了java.sql
、javax.sql
以及javax.transaction.xa
这些包。
咱们还可以在某个模块中从新导出它所依赖的模块中的API(或者是其中的一部分)。这将会对重构提供支持,咱们可以在不破坏依赖的状况下拆分或合并模块,由于最初的依赖能够继续存在。重构后的模块能够导出与以前相同的包,即使它们可能不会包含全部的代码。在极端的状况下,有一种所谓的#p#分页标题#e#聚合器模块(aggregator module),它能够根本不包含任何代码,只是做为一组模块的抽象。实际上,Java 8中所提供的compact profile就是这样作的。
从上面的例子中,咱们能够看到java.sql
从新导出了它依赖的API,即java.logging
和java.xml
。
为了帮助开发者(尤为是模块化JDK的人员)让他们所导出API的有较小的接触面,有一种可选的限制导出(qualified export)机制,它容许某个模块将一些包声明为只针对一组特定的模块进行导出。因此使用“标准”机制时,导出功能的模块并不知道(也不关心)谁会访问这些包,可是经过限制导出机制,可以让一个模块限定可能产生的依赖。
如前所述,JEP 200的目标之一就是模块可以在开发的各个阶段组合为各类配置。对于平台模块能够如此,这样就可以建立与完整JRE或JDK相似的镜像,Java 8所引入的compact profile以及包含特定模块集合(及其级联依赖)的任意自定义配置都使用了这种机制。相似的,开发人员也可使用这种机制来组合他们应用程序的不一样变种。
在编译期(compile-time),要编译的代码只能看到所配置的模块集合中导出的包。在构建期(build-time),借助一个新的工具(可能会被称为JLink),咱们可以建立只包含特定模块及其依赖的二进制运行时镜像。在安装期(launch-time),镜像可以看起来就像是只包含了它所具备的模块的一个子集。
咱们还可以替换实现了受权标准(endorsed standard)和独立技术(standalone technology)的模块,在任意的阶段都能将其替换为较新的版本。这将会替代已废弃的受权标准重载机制(endorsed standards override mechanism)以及扩展机制(参见下文。)#p#分页标题#e#
模块系统的各个方面(如依赖管理、封装等等),在全部阶段的运行方式是彻底相同的,除非由于特定的缘由,在某些阶段没法实现。
模块相关的全部信息(如版本、依赖以及包导出)都会在代码文件中进行描述,这样会独立于IDE和构建工具。
在模块系统中,借助强封装技术,可以很容易自动计算出一段特定的代码都用在了哪些地方。这会使得程序分析和优化技术更加可行:
快速查找JDK和应用程序的类;及早进行字节码的检验;积极级联(aggressive inlining)像lambda表达式这样的内容以及其余的编译器优化;构建特定于JVM的内存镜像,它加载时可以比类文件更加高效;预先将方法体编译为原生代码;移除没有用到的域、方法和类。——Jigsaw项目: 目标 & 需求(草案3)
有一些被称为全程序优化(whole-program optimization)的技术,在Java 9中至少会实现两种这样的技术。还有包含一个工具,使用这个工具可以分析给定的一组模块,并使用上述的优化技术,建立更加高性能的二进制镜像。
目前,要自动发现带有注解的类(如Spring注解标注的配置类),须要扫描特定包下的全部类。这一般会在程序启动的时候完成,这在至关程度上会减慢启动的过程。
模块将会提供一个API,容许调用者识别全部带有给定注解的类。一种预期的方式是为这样的类建立索引,这个索引会在模块编译的时候建立。#p#分页标题#e#
诊断工具(如栈跟踪信息)将会进行更新,其中会包含模块的信息。并且,它们还会集成到反射API中,这样就能按照操做类的方式来使用它们,还会包含版本信息,这一信息能够进行反射,也能够在运行时重载。
模块的设计可以���咱们在使用构建工具时“尽量地减小麻烦(with a minimum of fuss)”。编译以后的模块可以用在classpath中,也能做为一个模块来使用,这样的话,库的开发人员就没有必要为classpath应用和基于模块的应用分别建立多个构件了。
与其余模块系统的相互操做也进行了规划,这其中最著名的也就是OSGi。
尽管模块可以对其余的模块隐藏包,可是咱们依然可以对模块包含的类和接口执行白盒测试。
模块系统在设计时,始终考虑到了包管理器文件格式,“如RPM、Debian以及Solaris IPS”。开发人员不只可以使用已有的工具将一组模块集合建立为特定OS的包,这些模块还能调用按照相同机制安装的其余模块。
开发人员还可以将组成应用的一组模块打包为特定OS的包,“终端用户可以按照目标系统的通用作法,安装和调用所打成的包”。基于上述的介绍,咱们能够得知只有目标系统中不存在的模块才必需要打包进来。
正在运行中的应用可以建立、运行并发布独立的模块配置。在这些配置中,能够包含开发者和平台模块。对于容器类架构,这会很是有用,如IDE、应用服务器或其余Java EE平台。#p#分页标题#e#
按照Java的惯例,这些变动在实现时,会强烈关注到向后的兼容性,全部标准和非废弃的API及机制都可以继续使用。可是项目可能会依赖其余缺少文档的构造,这样的话,在往Java 9迁移的时候,就须要一些额外的工做了。
借助于强封装,每一个模块可以明确声明哪些类型会做为其API的一部分。JDK将会使用这个特性来封装全部的内部API,所以它们会变得不可用了。
在Java 9所带来的不兼容性中,这多是涵盖范围最大的一部分。可是这也是最明显的,由于它会致使编译错误。
那么,什么是内部API呢?毫无疑问,位于sun.*
包中的全部内容。若是位于com.sun.*
包中,或者使用了@jdk.Exported
注解,在Oracle JDK中它依然是可用的,若是没有注解的话,那么它就是不可用的了。
能产生特殊问题的一个样例就是sun.misc.Unsafe
类。它用在了不少项目中,用来实现关键任务或性能要求较高的代码,它未来可能不可用引起了不少的相关讨论。不过,在一次相关的交流中曾经提出,经过一个废弃的命令行标记,它依然是可用的。考虑到没法将其全部的功能都放到公开API中,这多是一种必要的权衡。
另一个样例是com.sun.javafx.*
包中的全部内容。这些类对于构建JavaFX控件是相当重要的,而且它们还有必定数量的bug要修改。这些类中的大多数功能都会做为发布的目标。
在具备可扩展的Java运行时以后,它容许咱们很灵活地建立运行时镜像,JDK和JRE就丧失了其独有的特性,它们只是模块组合中的两种形式而已。
这意味着,这两个构件将会具备相同的结构,包括目录结构也相同,任何依赖它(如在原来的JDK目录中会有名为jre的子目录)的代码就不能正常运行了。
像lib/rt.jar
和lib/tools.jar
这样的内部JAR将不可用了。它们的内容将会存储到特定实现的文件中,这些文件的格式还未明确说明,有可能会发生变化。
任何假设这些文件存在的代码将没法正确运行。这可能对IDE或其余严重依赖这些文件的工具带来一些切换的麻烦。
在运行时,有些API会返回针对类和资源文件的URL(如ClassLoader.getSystemResource)。在Java 9以前,它们都是jar URL,格式以下:
jar:file:<path-to-jar>!<path-to-file-in-jar>
Jigsaw项目将会使用模块做为代码文件的容器,单个JAR将不可用了。这须要一个新的格式,因此这些API将会返回jrt URL:
jrt:/<module-name>/<path-to-file-in-module>
若是使用这些API所返回的实例来访问文件的代码(如URL.getContent),那么运行方式会和如今同样。可是,若是依赖于jar URL的#p#分页标题#e#结构(好比手动构建它们或对其进行解析),那么就会出错了。
有一些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),名为#p#分页标题#e#JDeps (介绍了一些内部的包,官方有针对Windows以及Unix的文档),它可以列出某个项目依赖的全部的包。若是在运行时使用-jdkinternals
参数的话,那么它将会列出该项目所使用的几乎全部的内部API。
之因此说“几乎全部”是由于它还没法识别Java 9中不可用的全部的包。这至少会影响到JavaFX所属的包,能够查看JDK-8077349。(经过使用这个搜索,除了缺失的功能之外,我没能发现其余的缺陷。)
至少存在三个用于Maven的JDeps插件:分别由Apache、Philippe Marschall以及我本人所提供。就目前来说,最后一个是惟一当jdeps
-jdkinternals
报告中依赖内部API时,致使构建失败的插件。(如今,Apache的插件在出现内部API依赖时,也会提示构建失败,参见InfoQ的这篇新闻。——译者注)
Jigsaw项目最新的消息来源于Jigsaw-Dev邮件列表。我也会在博客中继续讨论这个话题。
若是你担忧某个特定的API在Java 9中不可用的话,那么你能够查看相关OpenJDK项目的邮件列表,由于他们会负责开发公开的版本。
Java 9的早期试用构建版本已经可用了。不过,JSR 376依然处于开发阶段,在这些构建版本中尚没法使用模块系统,还会有不少的变化。(在目前的试用版中,已经包含了Jigsaw,不过最新的消息是Java 9又要延期六个月发布了。——译者注)实际上,除了强封装之外,其余的功能都已经就绪了。
将收集到的消息发送给Jigsaw-Dev邮件列表可以反馈给项目。最后,引用JEP 220(临近)结尾的一段话:#p#分页标题#e#
咱们不可能抽象地肯定这些变动的所有影响,因此必需要依赖普遍的内部测试,尤为重要的还有外部测试。[……]若是有些变动会给开发人员、部署人员或终端用户带来难以承受的负担,那么咱们将会研究减小其影响的方式。
另外,还有一个全球的Java用户群组AdoptOpenJDK,它可以很好地将早期试用者联系起来。
Nicolai Parlog是一名软件开发人员,对Java充满热情。他不断地阅读、思考以及撰写与Java相关的内容,他靠编码为生,也以此为乐。他是多个开源项目的长期贡献者,并在CodeFX上用博客记录软件开发相关的内容。你能够在Twitter上关注Nicolai。