BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

Java 9终于要包含Jigsaw项目了

| 作者 Nicolai Parlog 关注 1 他的粉丝 ,译者 张卫滨 关注 13 他的粉丝 发布于 2016年2月3日. 估计阅读时间: 34 分钟 | 都知道硅谷人工智能做的好,你知道 硅谷的运维技术 也值得参考吗?QCon上海带你探索其中的奥义

当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运行时在实际使用某项依赖之前,并不能探测到这个依赖是无法满足的。如果出现这种情况,将会出现NoClassDefFoundError异常,进而导致正在运行的应用崩溃。

像Maven这样的构建工具能够帮助解决这个问题。

传递性依赖

一个应用程序要运行起来可能只需依赖几个库就足够了,但是这些库又会需要一些其他的库。问题组合起来会变得更加复杂,在所消耗的体力以及出错的可能性上,它会呈指数级地增长。

同样,构建工具能够在这个问题上提供一些帮助。

遮蔽

有时候,在classpath的不同JAR包中可能会包含全限定名完全相同的类,比如我们使用同一个库的两个不同版本。因为类会从classpath中的第一个JAR包中加载,所以这个版本的变种将会“遮蔽”所有其他的版本,使它们变得不可用。

如果这些不同的变种在语义上有所差别,那将会导致各种级别的问题,从难以发现的不正常行为到非常严重的错误都是有可能的。更糟糕的是,问题的表现形式是不确定的。这取决于JAR文件在classpath中的顺序。在不同的环境下,可能也会有所区别,例如开发人员的IDE与代码最终运行的生产机器之间就可能有所差别。

版本冲突

如果项目中有两个所需的库依赖不同版本的第三个库,那么将会产生这个问题。

如果这个库的两个版本都添加到classpath中的话,那么最终的行为是不可预知的。首先,因为前面所述的遮蔽问题,两个版本的类中,只会有一个能够加载进来。更糟糕的是,如果某个类位于一个JAR包中,但是它所访问的其他类却不在这个包中,这个类也能够加载。所导致的结果就是,对这个库的代码调用将会混合在两个版本之中。

在最好的情况下,如果试图访问所加载的类中不存在的代码,将会导致明显的NoClassDefFoundError错误。但是在最坏的情况下,版本之间的差别仅仅是在语义上,实际的行为会有细微的差别,这会引入很难发现的bug。

识别这种情况所导致的难以预料的行为是很困难的,也无法直接解决。

复杂的类加载机制

默认情况下,所有的类由同一个ClassLoader负责加载,在有些场景下,可能有必要引入额外的加载器,例如允许用户加载新的类,对应用程序进行扩展。

这很快就会导致复杂的类加载机制,从而产生难以预期和难以理解的行为。

在包之间,只有很弱的封装机制

如果类位于同一个包中,那Java的可见性修饰符提供了一种很棒的方式来实现这些类之间的封装。但是,要跨越包之间边界的话,那只能使用一种可见性:public

因为类加载器会将所有加载进来的包放在一起,public的类对其他所有的类都是可见的,因此,如果我们想创建一项功能,这项功能对某个JAR是可用的,而对于这个JAR之外是不可用的,这是没有办法实现的。

手动的安全性

包之间弱封装性所带来的一个直接结果就是,安全相关的功能将会暴露在同一个环境中的所有代码面前。这意味着,恶意代码有可能绕过安全限制,访问关键的功能。

从Java 1.1开始,有一种hack的方式,能够防止这种状况:每当进入安全相关的代码路径时,将会调用SecurityManager,并判断是不是允许访问。更精确地讲,它应该在每个这样的路径上都进行调用。过去,在有些地方遗漏了对它们的调用,从而出现了一些漏洞,这给Java带来了困扰。

启动性能

最后,Java运行时加载并JIT编译全部所需的类需要较长的时间。其中一个原因在于类加载机制会对classpath下的所有JAR执行线性的扫描。类似的,在识别某个注解的使用情况时,需要探查classpath下所有的类。

目标

Jigsaw项目的目标就是解决上面所述的问题,它会引入一个语言级别的机制,用来模块化大型的系统。这种机制将会用在JDK本身中,开发人员也可以将其用于自己的项目之中。

需要注意的是,对于JDK和我们开发人员来说,并不是所有的目标都具有相同的重要性。有一些与JDK具有更强的相关性,并且大多数都对日常的编程不会带来巨大的影响(这与最近的语言修改形成了对比,如lambda表达式和默认方法)。不过,它们依然会改变大型项目的开发和部署。

可扩展性的平台

JDK在模块化之后,用户就能挑出他们需要的功能,并创建自己的JRE,在这个JRE中只包含他们需要的模块。这有助于在小型设备和容器领域中,保持Java作为关键参与者的地位。

在这个提议的规范中,允许将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

提升性能

因为能够更加清晰地界定所使用代码的边界,现有的优化技术能够更加高效地运用。

很多预先(ahead-of-time)优化和全程序(whole-program)优化的技术会更加高效,因为能够得知某个类只会引用几个特定组件中的类,它并不能引用运行时所加载的任意类。 —— JSR 376

核心概念

因为模块化是目标,所以Jigsaw项目引入了模块(module)的概念,描述如下:

命名、自描述的程序组件,会包含代码和数据。模块必须能够包含Java类和接口,组织为包的形式,同时也能以动态加载库的形式(dynamically-loadable library)包含原生代码。模块的数据必须能够包含静态资源文件和用户可编辑的配置文件。 —— Java平台模块系统:需求(草案2)

为了能够基于一定的上下文环境来了解模块,我们可以想一下知名的库,如Google Guava Apache Commons中的库(比如Collections或IO),将其作为模块。根据作者希望划分的粒度,每个库都可能划分为多个模块。

对于应用来说也是如此。它可以作为一个单体(monolithic)的模块,也可以进行拆分。在确定如何将其划分为模块时,项目的规模和内聚性将是重要的因素。

按照规划,在组织代码时,模块将会成为开发人员工具箱中的常规工具。

开发人员目前已经能够考虑到一些标准的程序组件,如语言层面的类和接口。模块将会是另外一种程序组件,像类和接口一样,它们将会在程序开发的各个阶段发挥作用。 ——Mark Reinhold的文章“Project Jigsaw:将宏伟蓝图转换为可聚焦的点”

模块又可以进一步组合为开发阶段的各种配置,这些阶段也就是编译期、构建期、安装期以及运行期。对于我们这样的Java用户来说,可以这样做(在这种情况下,通常会将其称之为开发者模块),同时这种方式还可以用来剖析Java运行时本身(此时,它们通常称之为平台模块)。

实际上,这就是JDK目前进行模块化的规划。

(点击放大图像)

特性

那么,模块是如何运行的呢?查阅一下Jigsaw项目的需求以及 JSR 376将会帮助我们对其有所了解。

依赖管理

为了解决“JAR/Classpath地狱”的问题,Jigsaw项目的一个关键特性就是依赖管理。让我们看一下这些相关的组件。

声明与解析

模块将会声明它需要哪些其他的模块才能编译和运行。模块系统会使用该信息传递性地识别所有需要的模块,从而保证初始的那个模块能够编译和运行。

我们还可以不依赖具体的模块,而是依赖一组接口。模块系统将会试图据此识别模块这些模块实现了所依赖的接口能够满足依赖系统会将其绑定到对应的接口中。

版本化

模块将会进行版本化。它们能够标记自己的版本(在很大程度上可以是任意格式,只要能够完全表示顺序就行),版本还能用于限制依赖。在任意阶段都能覆盖这两部分信息。模块系统会在各个阶段都强制要求配置能够满足所有的限制。

Jigsaw项目不一定会支持在一个配置中存在某个模块的多个版本。但是,稍等,那该如何解决JAR地狱的问题呢? 好问题!

版本选择——针对同一个模块,在一组不同版本中挑选最合适的版本——并没有作为规范所要完成的任务。所以,在我撰写的上文中,模块系统会识别所需的模块进行编译,在运行时则可能会使用另外一个模块,这都基于一个假设,那就是环境中只存在模块的一个版本。如果存在多个版本的话,那么上游的步骤(如开发人员或者他所使用的构建工具)必须要做出选择,系统只会校验它能满足所有的约束。

封装

模块系统会在各个阶段强制要求强封装。这是围绕着一个导出机制实现的,在这种情况下,只有模块导出的包才能访问。封装与SecurityManager所执行的安全检查是相独立的。

这个提议的具体语法尚没有定义,但是JEP 200提供了一些关键语义的XML实例。作为样例,如下的代码声明了java.sql模块。

<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.basejava.logging以及java.xml。在稍后介绍不同的导出机制时,我们就能理解上文中其他的声明了。

导出

模块会声明特定的包进行导出,只有包含在这些包中的类型才能导出。这意味着其他模块只能看到和使用这些类型。更严格是,其他模块必须要显式声明依赖包含这些类型的模块,这些类型才能导出到对应的模块中

非常有意思的是,不同的模块能够包含相同名称的包,这些模块甚至还能够将其导出

在上面的样例中,java.sql导出了java.sqljavax.sql以及javax.transaction.xa这些包。

重新导出

我们还能够在某个模块中重新导出它所依赖的模块中的API(或者是其中的一部分)。这将会对重构提供支持,我们能够在不破坏依赖的情况下拆分或合并模块,因为最初的依赖可以继续存在。重构后的模块可以导出与之前相同的包,即便它们可能不会包含所有的代码。在极端的情况下,有一种所谓的聚合器模块(aggregator module),它可以根本不包含任何代码,只是作为一组模块的抽象。实际上,Java 8中所提供的compact profile就是这样做的。

从上面的例子中,我们可以看到java.sql重新导出了它依赖的API,即java.loggingjava.xml

限制导出

为了帮助开发者(尤其是模块化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和构建工具。

性能

全程序优化的技术

在模块系统中,借助强封装技术,能够很容易自动计算出一段特定的代码都用在了哪些地方。这会使得程序分析和优化技术更加可行:

快速查找JDK和应用程序的类;及早进行字节码的检验;积极级联(aggressive inlining)像lambda表达式这样的内容以及其他的编译器优化;构建特定于JVM的内存镜像,它加载时能够比类文件更加高效;预先将方法体编译为原生代码;移除没有用到的域、方法和类。——Jigsaw项目: 目标 & 需求(草案3)

有一些被称为全程序优化(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呢?毫无疑问,位于sun.*包中的所有内容。如果位于com.sun.*包中,或者使用了@jdk.Exported注解,在Oracle JDK中它依然是可用的,如果没有注解的话,那么它就是不可用的了。

能产生特殊问题的一个样例就是sun.misc.Unsafe类。它用在了很多项目中,用来实现关键任务或性能要求较高的代码,它将来可能不可用引发了很多相关讨论。不过,在一次相关的交流中曾经提出,通过一个废弃的命令行标记,它依然是可用的。考虑到无法将其所有的功能都放到公开API中,这可能是一种必要的权衡。

另外一个样例是com.sun.javafx.*包中的所有内容。这些类对于构建JavaFX控件是至关重要的,并且它们还有一定数量的bug要修改。这些类中的大多数功能都会作为发布的目标

合并JDK和JRE

在具有可扩展的Java运行时之后,它允许我们很灵活地创建运行时镜像,JDK和JRE就丧失了其独有的特性,它们只是模块组合中的两种形式而已。

这意味着,这两个构件将会具有相同的结构,包括目录结构也相同,任何依赖它(如在原来的JDK目录中会有名为jre的子目录)的代码就不能正常运行了。

内部JAR不可用了

lib/rt.jarlib/tools.jar这样的内部JAR将不可用了。它们的内容将会存储到特定实现的文件中,这些文件的格式还未明确说明,有可能会发生变化。

任何假设这些文件存在的代码将无法正确运行。这可能对IDE或其他严重依赖这些文件的工具带来一些切换的麻烦。

针对运行时镜像内容的新URL模式

在运行时,有些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的结构(比如手动构建它们或对其进行解析),那么就会出错了。

移除授权标准重载机制

有一些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的文档),它能够列出某个项目依赖的所有的包。如果在运行时使用-jdkinternals参数的话,那么它将会列出该项目所使用的几乎所有的内部API。

之所以说“几乎所有”是因为它还无法识别Java 9中不可用的所有的包。这至少会影响到JavaFX所属的包,可以查看 JDK-8077349。(通过使用这个搜索,除了缺失的功能以外,我没能发现其他的缺陷。)

至少存在三个用于Maven的JDeps插件:分别由ApachePhilippe 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(临近)结尾的一段话:

我们不可能抽象地确定这些变更的全部影响,所以必须要依赖广泛的内部测试,尤其重要的还有外部测试。[……]如果有些变更会给开发人员、部署人员或终端用户带来难以承受的负担,那么我们将会研究减少其影响的方式。

另外,还有一个全球的Java用户群组AdoptOpenJDK,它能够很好地将早期试用者联系起来。

关于作者

Nicolai Parlog是一名软件开发人员,对Java充满热情。他不断地阅读、思考以及撰写与Java相关的内容,他靠编码为生,也以此为乐。他是多个开源项目的长期贡献者,并在CodeFX上用博客记录软件开发相关的内容。你可以在Twitter上关注Nicolai。

查看英文原文:Project Jigsaw is Really Coming in Java 9

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

Java9 jigsaw by soft wmz

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT