Java 9 将会从默认类路径中去除 CORBA

作为模块系统的一部分正在进行的迁移,CORBA和其他的Java EE模块在Java
9更新时将不会包含在默认的类路径中。这些模块依然能够获取,但是开发者们需要使用特定的命令行指令才能够使用它们。这个变化只会影响到非模块化的、以Java
9为目标系统的应用,因为模块化的应用已经需要详细指出它们依赖的模块。

2.1 模块路径

为了定位定义在工件中的模块,模块系统需要搜索主机中定义的模块路径澳门新萄京官方网站,。模块路径是一个序列,其中每个元素要么是模块工件,要么是包含模块工件的目录。模块系统会按顺序搜索模块路径中的元素去寻找第一个满足条件的模块。

模块路径跟类路径有着本质上的区别,并且更加健壮强大。类路径天生的脆性是由于它只是定位所有工件中单个类型的手段,而不能区分工件本身的不同。这对于提前判断工件是否缺失尤其重要。它也允许不同的工件在相同的包中定义类型,即便这些工件是同一个程序组件的不同版本甚至是完全不同的组件。

相反,模块路径是用来定位整个模块而不是单个类型的手段。如果模块系统无法在模块路径中找到某个特定的工件依赖,或者在同一个目录下遇到两个相同名称的工件,那么编译器或者虚拟机将会报告该错误并且退出。

编译期或者运行时内置的模块,以及模块路径中定义的工件统称为可观察到的模块

那些想要迁移至Java
9但是还没有做好重写他们应用每个模块系统的准备的人可以仍然将这些依赖添加回类路径,通过在编译或运行应用时使用命令行指令“-addmods
”。

3 兼容性&迁移

到目前为止,我们已经看到了如何从头开始定义模块,将它们打包成模块工件,并且把他们与其他平台内置的模块或者定义在其他工件中的模块一起使用。

当然,大部分的Java代码是在模块系统引入前就写好的,并且还必须像现在这样不需任何改变依旧能正常运行。因此,即使平台本身是由模块组成,模块系统也仍然可以编译运行由类路径中的Jar文件组成的应用。并且也可以将先用的应用以一种灵活渐进的方式迁移到模块化中来。

不幸的是,未命名模块没有任何明确的依赖关系,这会给模块分解带来很大的挑战。在一个模块化的Java应用中,正在被编译的模块会有一个明确的所需
要的依赖关系清单。编译器可以使用这些信息,伴随着依赖关系中的依赖,来计算出一个图表,其中包括所有被编译模块直接或间接需要的所有模块的传递闭包。而
未命名模块没有标明任何依赖,所以它不能被用于计算模块图表的根节点。

3.4 类路径桥接

许多现存的JAR文件可以被用作自动模块,但是有些却不能。如果类路径下有多于两个JAR文件有相同的包,那么最多只有一个可以被用作自动模块,因为模块系统要确保每个命名模块最多读取一个定义了特定包的模块,以及定义相同包的命名模块不会互相干扰。在这种情况下,通常只需要一个JAR文件。如果其他重复或者近似重复的,不小心放到了类路径下,那么可以将一个用作自动模块并且丢弃其他的。然而如果类路径上的多个JAR文件有意地包含相同包中的类型,那么他们必须被保留在类路径上。

为了能够在多个JAR文件无法被用作自动模块时依旧可以迁移,我们可以用自动模块作为显示模块中的代码与类路径中的代码的桥梁:除了读取所有的命名模块,自动模块还会读取未命名模块。例如,如果我们应用的原始类路径包含了org-baz-fiz.jar跟org-baz-fuz.jar文件,那么我们得到下图:

澳门新萄京官方网站 1

就像前面提到的,未命名模块会导出它的所有包,因此自动模块中的代码可以访问类路径中加载的所有公共类型。

使用类路径中某类型的自动模块必须不能将这些类型暴露给依赖它的显示模块,因为显示模块不能声明对未命名模块的依赖。例如,如果显示模块com.foo.app中的代码引用了com.foo.bar中的公共类型,并且该类型的方法签名引用了依旧在类路径JAR文件中的类型,那么com.foo.app中的代码将无法访问这些类型,因为com.foo.app不能依赖未命名模块。可以临时将com.foo.app当作自动模块来补救这一点,以便它的代码可以访问类路径中的类,直到类路径中的相关JAR文件可以被当作自动模块或者转换为显示模块。

正如“模块系统的情形”
中解释的那样,模块化在Java
9中的附加功能不会强制开发者直接使用它们,反而,Java
9中提供了一定数量的向后兼容选项。其中一个允许在Java
9中完成传统的、非模块化的代码。其中为了它能够正常工作,非模块化的代码需要被添加至编译器,作为一个特殊的模块,叫做“未命名模块”,它默认显示了所
有包含的包并且没有详述的依赖。

2 使用模块

个人模块可以在模块工件中定义,也可以内置于编译期或者运行时环境中。为了在任意阶段都可以使用他们,模块系统需要定位它们,然后确定相互依赖关系,以便提供可靠的配置以及强封装性。

为了修正这个问题,Java
9将会使用一些默认值作为将代码编译入未命名模块的根模块。在最新的修改中,这个默认根模块已经从java.se.ee迁移到java.se,这意味着所有的Java
EE扩展现在都默认不能使用了。这个修改会帮助预防包含它们自己的Java
EE包实现的应用服务器中的冲突。

2.5 隐式可读性

如果一个模块可以读取另一个模块,某些情况,逻辑上也应该可以读取其他模块。

例如,平台的java.sql模块依赖java.logging跟java.xml模块,不仅因为它的代码实现中使用了这些模块中的类型,而且还因为它定义的类型的方法签名引用了这些模块中的类型。
java.sql.Driver接口中声明了下面这个公共方法:

public Logger getParentLogger();

其中Logger是java.logging模块中导出的java.util.logging包中声明的类型。

假设,com.foo.app模块中的代码为了获取logger并且记录日志而调取了这个方法:

String url = ...;
Properties props = ...;
Driver d = DriverManager.getDriver(url);
Connection c = d.connect(url, props);
d.getParentLogger().info("Connection acquired");

如果com.foo.app模块的声明像上面提到的那样,那么这段代码将不能工作:getParentLogger方法返回一个Logger,它是一个在java.logging模块中声明的类型,它对com.foo.app模块是不可读的,因此上面对Logger类中的info方法的调用在编译期跟运行时都将失败,因为该类不可访问,所以该方法同样不可访问。

该问题的一个解决方案是所有依赖java.sql模块并且包含使用getParentLogger方法返回的Logger对象的代码的模块作者记得声明一个对java.logging模块的依赖。当然这种方式是不可靠的,因为它打破了最少意外的原则:如果一个模块依赖第二个模块,那么很自然的希望每个需要使用第一个模块的类型,即使是在第二个模块中定义的类型,都将对于仅仅依赖第一个模块的模块是直接可访问的。

因此我们拓展模块的声明以便一个模块可以将它所依赖的其他模块的可读性授予依赖它的任何模块。这种隐式的可读性通过在requires语句中包含一个public修饰符来表达。java.sql模块的声明实际上是这样:

module java.sql {
    requires public java.logging;
    requires public java.xml;
    exports java.sql;
    exports javax.sql;
    exports javax.transaction.xa;
}

public修饰符意味着任何依赖java.sql的模块不仅可读取java.sql模块,而且也可以读取java.logging跟java.xml模块。因此com.foo.app模块的模块图将包含两条用绿色边连接到java.sql模块的深蓝色的边,因为它们是因该模块而隐式可读的:

澳门新萄京官方网站 2

现在com.foo.app模块可以包含访问java.logging及java.xml模块导出包中所有公共类型的代码了,即使它的声明中没有提到这两个模块。

通常,如果一个模块导出一个包,该包包含一个签名引用第二个模块的类型,那么第一个模块的声明应该包含一个对第二个模块的requires public依赖。这样可以确保其它依赖第一个模块的模块自动的对第二个模块具有可读性,以及可以访问该模块导出包中的所有公共类型。

稿源:infoQ

1.4 平台模块

Java SE 9 平台规范将使用模块系统把平台划分为一系列模块。Java SE
9平台的实现可能包含所有的平台模块,也可能只包含其中的一部分。

任何情况下,模块系统中唯一明确知道的模块是被叫做java.base的基模块。基模块定义并导出了所有的平台核心包,同样包括它自身:

module java.base {
    exports java.io;
    exports java.lang;
    exports java.lang.annotation;
    exports java.lang.invoke;
    exports java.lang.module;
    exports java.lang.ref;
    exports java.lang.reflect;
    exports java.math;
    exports java.net;
    ...
}

基模块将会一直存在,任何其他模块都将隐式的依赖于基模块,然而基模块不会依赖于其他任何模块。

其他的平台模块将使用java.前缀,并且可能包含像用来连接数据库的java.sql模块,用来处理XML的java.xml模块,以及日志处理的java.logging模块。Java
SE
9平台规范中没有定义的模块,而在JDK中定义的模块,习惯上将会使用jdk.前缀。

3.1 未命名模块

如果有个需求是在任意已知的模块中加载一个没有定义包的类型,那么模块系统会尝试从类路径中加载它。如果加载成功,那么会被认为是一个特殊的被称为未命名模块的成员,以便确保每个类型关联到某个模块上。未命名模块就像是高级层面上的现有的未命名包的概念。当然,以后我们就把那些有名称的模块称作命名的模块

未命名的模块可以读取其他任意模块。因此从类路径中加载的任意类型中的代码都将可以访问任意其他可读模块的导出类型,这些可读模块默认包括命名模块,内置的平台模块。因此,在Java
SE 8上编译和运行的现有类路径应用程序将在Java SE
9上以完全相同的方式进行编译和运行,只要它只使用了标准的,不被废弃的Java
SE API即可。

未命名模块会导出它的所有包。就像我们将在下面看到的,这会使得迁移更加灵活。然而,这不意味着命名模块中的代码可以访问未命名模块中的类型。事实上,命名模块甚至不能声明对未命名模块的依赖。这个限制是有意为之的。因为允许命名模块依赖类路径中的任意内容是不可能做到可靠的配置的。

如果一个包被定义在了命名模块跟未命名模块中,那么未命名模块中的包会被忽略。即使面对类路径的混乱这依旧保持可靠的配置,即确保每一个模块依旧最多只会读取一个定义特定包的模块。如果在我们上面的例子中,一个类路径下的JAR文件包含com/foo/bar/alpha/AlphaFactory.class类,那么该文件将永远不会被加载,因为com.foo.bar.alpha包是com.foo.bar模块的导出包。

1. 定义模块

模块声明.模块工件.模块描述符.平台模块

5.4 未命名模块

我们前面学到如果一个类型不是定在在命名的,可观察的模块中,那么它就是未命名模块中的成员,但是这个未命名模块所关联的类加载器是哪个呢?

事实上,每个类加载器都有唯一的未命名模块,可以通过ClassLoader::getUnnamedModule新方法获得。如果一个类加载器加载了一个未命名模块中的类型,那么这个类型会被认在该类加载器的未命名模块中,例如,该类型的Class对象的getModule方法会返回它的类加载器的未命名模块。因此,被简称为“未命名模块”的模块其实是application类加载器的未命名模块,它从类路径加载那些没有定义在已知模块中的类型。

4 服务

通过服务接口与服务提供者实现程序组件间的松耦合是大型软件系统的强大工具。Java一直通过java.util.ServiceLoader来支持服务,它在运行时通过搜索类路径来定位服务提供者。对于模块中定义的服务提供者,我们必须考虑如何在诸多可观察模块中定位这些模块来解决它们之间的依赖,并且使得那些使用相应服务的代码可用。

例如,假设我们的com.foo.app模块使用MySQL数据库,并且MySQL
的JDBC驱动是在如下可观察模块中提供的:

module com.mysql.jdbc {
    requires java.sql;
    requires org.slf4j;
    exports com.mysql.jdbc;
}

其中org.slf4j是驱动中使用的日志库,com.mysql.jdbc是包含java.sql.Driver服务接口实现的包。(没有必要导出驱动包,这里只是为了清晰)

为了使java.sql模块可以使用该驱动,ServiceLoader类必须可以通过反射实例化该驱动类;为了实现这一点,模块系统必须将驱动模块添加到模块图中并解决它的依赖,因此:

澳门新萄京官方网站 3

为了完成这点,模块系统必须可以通过之前解析的模块来识别任何服务的使用,然后从诸多可观察模块中定位并解析提供者。

模块系统可以通过扫描模块工件中的类文件来调用ServiceLoader::load
方法来识别服务的使用,但是这样即慢又不可靠。一个模块使用特定的服务是该模块定义的基本面,因此为了效率和清晰度,我们在模块定义中使用use语句表达这一点:

module java.sql {
    requires public java.logging;
    requires public java.xml;
    exports java.sql;
    exports javax.sql;
    exports javax.transaction.xa;
    uses java.sql.Driver;
}

就像ServiceLoader类目前所做的一样,模块系统可以通过扫描模块工件的META-INF/services资源条目来识别服务提供者。一个模块提供一个特定服务的实现同样的重要,因此我们在模块声明中使用provides语句来表达这一点:

module com.mysql.jdbc {
    requires java.sql;
    requires org.slf4j;
    exports com.mysql.jdbc;
    provides java.sql.Driver with com.mysql.jdbc.Driver;
}

现在,通过简单的阅读这些模块的声明可以看到其中一个使用了另一个提供的服务。

在模块声明中声明模块提供与模块使用关系不仅仅是提供效率跟清晰度。
这两种服务声明可以在编译期被解析来确保服务接口(如java.sql.Driver)可以被提供者及服务使用者访问到。服务提供者声明可以被进一步解析来确保提供者(如com.mysql.jdbc.Driver)确实实现了声明的服务接口。最后,可以使用预先编译和链接工具来解析服务使用声明以确保可观察的提供者在运行之前被正确的编译和链接。
出于迁移的目的,如果定义自动模块的JAR文件包含META-INF/services资源条目,那么每个条目都会被假设在该模块下声明了相应的provides语句。自动模块可以使用任意的可用服务。

总结

模块系统的说明

该文章是关于JSR 376: The Java Platform Module
System中提议的Jigsaw项目原型的非正式版概览。另一篇文章描述了JDK相关工具以及API的一些增强特性,不过这不在JSR的范围之内了。

就像JSR376中描述的一样,模块化系统的目标是提供:

  • 可靠的配置,用程序组件的方式来替代脆弱的、易出错的classpath机制,并且可以显示的声明对其他组件的依赖。
  • 强封装,允许组件声明哪些类型可以对其它组件开放,哪些不可以。

这些特性将会为应用开发者,库开发者,以及Java
SE平台的实现人员带来直接跟间接的好处,因为它们将使得系统具有更好的拓展性,更高的完整性,以及更高的性能。

5. 高级话题

映射.映射可读性.类加载器.未命名的模块.层.有限制的导出

3.3 自动模块

自下而上的迁移是直截了当的,当并不可能总是如此。即使org-baz-qux.jar的维护者还没有将它转化为合适的模块-或者永远不会将它模块化-我们依旧想模块化我们自己的com-foo-app.jar和com-foo-bar.jar组件。

我们已经知道com-foo-bar.jar中的代码引用org-baz-qux.jar中的类型。如果我们把com-foo-bar.jar转换为命名模块com.foo.bar,但是把org-baz-qux.jar留在类路径中,那么会导致代码不可用:org-baz-qux.jar中的类型会被定义到未命名模块中,而com.boo.bar是命名模块,是无法依赖于未命名模块的。

因此我们必须以某种方式让org-baz-qux.jar以命名模块的方式运行,以便com.foo.bar可以依赖它。我们可以fork一个org.baz.qux源码的分支然后我们自己将其模块化,但是维护者不愿将它合并到上游仓库中,那么就不得不一直维护这个分支。

相反,我们可以将org-baz-qux.jar作为一个自动模块,原封不动的将其放到模块路径中而不是类路径下。这样将会定义一个可观察的模块,它的名字将由JAR文件衍生而来org.baz.qux,以便非自动模块可以用常规的方式依赖它:

澳门新萄京官方网站 4

自动模块是一个隐式定义的命名模块,因为它没有模块声明。相比之下,一个普通的命名模块会有模块声明来显示的定义;我们以后把这类模块看作显示模块。

没有好办法可以提前告知自动模块可能依赖哪些其他模块。因此在一个模块图被确定之后,自动模块可以读取任意其他的命名模块,无论自动还是显示:

澳门新萄京官方网站 5

(这些新的可读性边确实在模块图中造成了回路,使得它有些更加难懂了,但是我们把这看作是更加灵活迁移的可容忍的结果。)

类似的,没有好的办法去判断一个自动模块中的包会被其他模块或者仍在类路径中的类使用。因此,自动模块中的每个包都会被导出,即使实际上它只被内部使用:

澳门新萄京官方网站 6

最后,没有好的办法判断自动模块中是否有导出包中包含某些类型,它的方法签名中引用了其他自动模块中的类型。例如,我们首先模块化com.foo.app。并且将com.foo.bar和org.baz.qux都当作自动模块,那么我们将会得到下面模块图:

澳门新萄京官方网站 7

不读取相关的JAR文件中的所有类文件,是不能知道com.foo.bar中的公共类型是否声明了一个返回类型是org.baz.qux中的公共方法。因此,自动模块被授予对其它所有自动模块的隐式可读性:

澳门新萄京官方网站 8

现在,com.foo.app中的代码可以访问org.baz.qux中的类型,尽管我们知道实际上并不是这么做的。

自动模块提供了一个类路径的混乱与显示模块的严格的中间方案。就像上面看到的,他们允许一个由JAR文件组成的现有应用可以以自上而下,或者结合自上而下与自下而上的方式迁移到模块化中来。一般来讲,我们从一组任意类路径下的JAR文件组件开始,使用jdeps工具来分析他们的相互依赖,将那些我们可以控制源码的组件转换为显示模块,并且与剩余的JAR文件一起放到模块路径下。那些不能控制源码的JAR文件组建会被当作自动模块直到有一天他们也被转换为显示模块。

5.5 层

模块系统不会强制规定模块与类加载器之前的关系,但是为了加载特定的类型,必须能够以某种方式找到一个合适的加载器。因此,模块图在运行时的实例化会产生一个,它将图中每个模块映射到唯一的负责加载该模块中类型的类加载器。就像前面讨论的,boot层是JVM在启动时通过解析应用的初始模块,而不是可观察模块来创建的。

大部分的应用,以及现存的所有应用将不会用到除了boot层以外的其它层。然而复杂应用可以通过插件或者容器架构使用多层,像应用服务器,IDE,以及测试工具。这些应用可以使用动态的类加载以及模块系统反射API,加载和运行托管的应用,这些应用包含一个或多个模块。然而,还需要额外的两个灵活性:

  • 某个托管应用可能需要某个已经存在的模块的不同版本。例如,一个Java
    EE的web应用,可能需要JAX-WS栈在java.xml.ws模块中的版本,而不是运行时环境中内置的版本。
  • 某个托管应用需要的服务提供者可能不是已经发现的提供者。托管系统甚至会嵌入自己倾向的提供者。例如,一个web应用可能包含Woodstox
    streaming XML
    parse版本的一个副本,这种情况下,ServiceLoader类应该返回这个提供者,而不是其它的。

容器应用可以在现存层的上面为托管应用创建一个新层,通过解析应用的初始模块而不是一个可观察模块的不同空间。这个空间可以包含可平台可升级模块或者其他模块的备用版本,非平台模块存在于更低的层中;解析器会为这些备用模块设置优先级。这样一个空间也可以包含不同的服务提供者而不是哪些已经在低层被发现的;ServiceLoader类会在从低层返回提供者之前加载并且返回相应的提供者。

层可以叠加:一个新层可以构建在boot层上,并且其它层也可以构建在它之上。正常的解析过程的结果是某一层上的模块可以读取位于该层以及低于该层中的模块。因此某一层的模块图可以按引用包含比它低的每一层的模块图。

2.4 可访问性

模块图中定义的可读性关系以及模块声明中的exports语句是强封装性的基础。Java编译器跟虚拟机认为一个模块的某个包中的公共类型对其他模块中的代码可访问的条件是第一个模块对第二个模块是可读的,并且第一个模块导出了那个包。比如,两个类型S跟T定义在两个不同的模块中,并且T是公共的,那么S可以访问T的条件是:

  1. S所在模块可读去T所在模块,并且
  2. T所在模块导出了T所在的包。

就像私有方法跟私有属性不可被其它类访问一样,一个类型是无法透过不可访问的模块边界被引用的。任何尝试对它访问都会得到一个编译器报告的错误,或者虚拟机抛出的IllegalAccessError,或者反射运行时API抛出的IllegalAccessException。因此即使当一个类型被定义为公共的,但是假如它所在的包没有在模块声明中被导出,那么它也只能被本模块内的代码访问。

如果透过模块边界,一个方法或者属性的外围类是可以访问的,并且该成员本身的声明也是允许访问的,那么它也可以透过模块边界被访问。

来看一下上面的模块图中的强封装性是如何工作的,我们给每个模块贴上它所导出的包的标签:

澳门新萄京官方网站 9

模块com.foo.app模块中的代码可以访问com.foo.bar.alpah包中的公共类型,因为com.foo.app依赖于它,因此可读取com.foo.bar模块,并且com.foo.bar模块导出了com.foo.bar.alpah包。
如com.foo.bar包含一个内部包com.foo.bar.internal,那么com.foo.app中的代码不能访问该包中的任何类型,因为com.foo.bar没有导出它。com.foo.app中的代码不能访问org.baz.qux包中的类型,因为coom.foo.app不依赖它,因此不可读去该模块。

4. 服务

5.2 映射可读性

框架是一种在运行时使用反射来加载,检查,实例化其他类的工具。Java
SE平台自身的框架例子包括服务加载器,资源束,动态代理,以及序列化,当然还有很多流行的外部框架库,像是数据库持久,依赖注入和测试。

运行时发现的类,框架必须能够访问它的某个构造器才能实例化它。然而事情并不总是这样。

例如,平台的streaming XML
parser,通过javax.xml.steam.XMLInputFactory系统属性来加载并实例化XMLInputFactor服务的实现,如果定义了,则优先于通过ServiceLoader类来发现发现提供者。忽略异常处理和安全检查,代码大致像这样:

String providerName
    = System.getProperty("javax.xml.stream.XMLInputFactory");
if (providerName != null) {
    Class providerClass = Class.forName(providerName, false,
                                        Thread.getContextClassLoader());
    Object ob = providerClass.newInstance();
    return (XMLInputFactory)ob;
}
// Otherwise use ServiceLoader
...

在模块化环境下,只要包包含对context类加载器可知的提供者类,那么对Class:forName的调用就可以正常工作。然而,通过反射newInstance方法对提供者类构造器的调用就没那么幸运了。提供者可能是从类路径中加载的,这种情况下它位于未命名模块内,或者一些明明模块内,但是不管哪种情况,框架自身是在java.xml模块中的。该模块仅仅依赖于基模块,因此其他模块中的提供者对框架来说是不可访问到的。

为了使提供者类对框架是可访问的,我们需要让提供者模块对框架模块是可读的。我们可以允许所有框架显示的将必要的可读性边在运行时动态的添加到模块图中来,就像本文档之前的版本描述的一样,但是经验告诉我们,这种方式太麻烦,并且碍于迁移。

因此,取而代之,我们简单的修订了反射API,基于这样一种假设:任何反射某些类型的代码都在能够读取这些类型所在模块的模块中。这使得上面的例子以及跟此例相同的代码可用,并且不需要做任何改动。这种方式并不会削弱强封装性:如果公共类型想要被其他模块访问,无论是从编译的代码中,还是通过反射,它都必须在模块的导出包中。

网站地图xml地图