liubo-tech.github.io

热爱技术 从我做起

View on GitHub

JAVA9模块化详解(二)——模块的使用 返回

二、模块的使用

各自的模块可以在模块工件中定义,要么就是在编译期或者运行期嵌入的环境中。为了提供可靠的配置和强健的封装性,在分块的模块系统中利用他们,必须确定它们的位置,然后决定他们如何关联彼此。

2.1 模块的路径

为了在确定在工件中定义的模块的位置,模块系统搜索模块的路径,它在主系统中定义。模块路径是一个序列,它的每一个元素要么是一个模块工件,要么是一个包含模块工件的目录。模块路径中的元素被第一个工件有序的搜索,这个工件定义了一个合适的模块。模块的路径在物理上不同于类路径,而且更强大。类路径天生的脆弱性是它定位了路径下所有工件中的个体类型,在工件中间没有任何的区分。这使得它在工件丢失时不可能提前告知,它也允许不同的工件在相同的包中定义类型,即使那些工件代表中相同程序组件的不同版本,或者完全不同的组件。

相比之下,模块路径定位整个的模块,而不是个体类型。如果模块系统不能从模块路径中处理工件的特殊依赖,或者如果它在相同的目录下遇到了两个模块名字相同的工件,这是编译器或者虚拟机将报告一个错误并退出。

嵌入到编译器或运行期环境的模块,连同模块路径下工件定义的模块被交付到全局的可被观察的模块。

2.2 解决方案

假设我们有一个应用,它使用了上一章讲到的com.foo.bar模块,也用到了java.sql模块,包含了应用核心的模块声明如下:

module com.foo.app {
    requires com.foo.bar;
    requires java.sql;
}

给出的这个最初的应用模块,模块系统解决了requires项表达的依赖,它通过定位额外可观察到的模块完成了依赖的任务,然后解决那些模块的依赖,等等,直到每一个模块的每一个依赖都解决完。这个传递闭包计算的结果是一个模块图,它包含了从第一个模块到第二个模块的矢量,依赖的每一个模块通过一些其他的模块解决。

为了构建com.foo.bar模块的模块图,模块系统检测到了java.sql的模块声明,如下:

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

它也检测到了com.foo.bar的模块声明,在上面已经展示过,同时也检测到了org.baz.qux,java.logging,和java.xml模块,为了简洁,后三个模块在这里不做展示,它们也没有声明依赖其他模块。基于这些模块的声明,com.foo.app模块计算出的模块图包含如下的节点和边界:

image1

在上面的图中,深蓝色的线代表着确定的依赖关系,它在requires项中定义,浅蓝色的线代表着隐性的依赖,每一个模块都依赖基础模块(base module)。

2.3 可读性

在模块图中,当一个模块直接依赖另一个时,第一个模块中的代码可以访问第二个模块中的类型。因此,我们说第一个模块读取第二个模块,相等的,也可以说第二个模块对于第一个模块是可读的。于是,上面的图中,com.foo.app模块读取com.foo.bar和java.sql,但是不读取org.baz.qux,java.xml和java.logging。java.logging模块对于java.sql模块是可读的,对于其他模块不可读。

在模块图中可读性定义的关系是可靠性配置的基础:模块系统确保每一个依赖都被确定的其他的模块解决,模块图是非循环的,每一个模块最多读取一个模块定义的包(package),定义了相同名字的模块彼此间互不干扰。

可靠性配置不仅仅是更可靠,它也更快。当一个模块中的代码涉及到一个包(package)中的类型时,这个包肯定被定义在这个模块中,或者这个模块确切读取的其他模块中。因此,当寻找确切类型的定义时,不需要在多个模块中寻找,也不需要更糟的在类路径下寻找。

2.4 可接入性

模块中定义的可读性关系,结合了模块声明中的exports项,是强健的封装性的基础:java编译器和虚拟机认为,只有当第一个模块被其他模块读取时,第一个模块包中的公共类型才能被其他包访问,按照这个意思,第一个模块输出了可访问的包。如果S和T两个类型定义在不同的模块中,T是公共的(public),S中的代码可以访问T的要求如下:

一个类型用这种方式引用了不可访问的模块边界是不可用的,比如它访问的私有的方法或变量是不可用的。任何使用它的尝试都会引起编译器报告错误,或者被虚拟机抛出一个IllegalAccessError的错误,或者被运行期的反射API抛出IllegalAccessException 的错误。因此,一个类型即使是public,但是它并没有在模块声明中输出(exports),它也只能在自己的模块中被访问。

如果一个模块的封装类型是可以访问的或者成员的声明是可以访问的,则通过模块边界引用的方法和字段也是可以访问的。

为了看到强大的封装性是如何工作的,我们在上面的模块图中,添加了标注

image2

com.foo.app模块中的代码可以访问com.foo.bar.alpha包中的公共类型,英文com.foo.app依赖,也可以说读取com.foo.bar模块,并且英文com.foo.bar输出com.foo.bar.alpha包。如果com.foo.bar包含了一个内部的包com.foo.bar.internal,并且com.foo.bar不输出它,则com.foo.app不能访问这个包中的任何类型。com.foo.app中的代码不能涉及org.baz.qux中的类型,英文com.foo.app不依赖org.baz.qux。

2.5 隐性可读性

如果一个模块读取另一个,在某种情况下,它在逻辑上也读取其他的一些模块。举个例子,平台模块java.sql依赖java.logging和java.xml模块,它不仅使用了那些模块中的类型实现了代码,并且还定义了那些模块中的类型。比较特殊的,java.sql.Driver接口定义了公共方法:

public Logger getParentLogger();

在这里,Logger是java.logging模块中java.util.logging包输出的类型。假如com.foo.app模块引用了这个方法,并且打印了日志:

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模块并且使用Logger类时,在声明一个java.logging的依赖。这种方法是不可靠的,它违反了最少意外的原则:如果一个模块依赖了第二个模块,它非常自然的希望,第一个模块需要使用的类型,即使这个类型定义在第二个模块中,其他模块仅仅依赖第一个模块就可以直接访问。

于是,我们扩展了模块声明,一个模块可以把可读性授权给另外的,依赖它的模块,扩展可读性的表达式(public)在requires项上,如下所示:

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模块。如图:

image3

现在com.foo.app模块可以读取java.xml和java.logging模块中的所有公共类型,虽然它的声明中并没有提到那些模块。

总的来说,如果一个模块输出一个包,这个包包含了第二个模块中的包的类型,则第一个模块应该声明为 requires public ,依赖第二个模块。这可以保证其他依赖了第一个模块的模块可以自动读取第二个模块,因此,可以访问那个模块输出包中的所有公共类型。

至此,JAVA9模块化详解(二)——模块的使用就先介绍到这里,这个系列还会继续,请大家多多支持,有不妥之处,还请大家多多交流。