java9新特性-模块化

  • 更可靠的配置,通过制定明确的类的依赖关系代替以前那种易错的类路径(class-path)加载机制。

  • 强大的封装,允许一个组件声明它的公有类型(public)中,哪些可以被其他组件访问,哪些不可以。

定义模块化

为了提高可靠的配置性和强大的封装性,我们将模块化看作是java程序组件的一个基本的新特性,这样它对开发者和可支持的工具更加友好。一个模块是一个被命名的,代码和数据的自描述的集合。它的代码有一系列包含类型的包组成,例如:java的类和接口。它的数据包括资源文件(resources)和一些其他的静态信息

模块的声明

一个模块的自描述表现在它的模块声明中,它是java程序语言中的一个新的结构,最简单的可能的模块声明仅仅是指定模块的名字

module com.foo.bar { }

一个或更多个requires项可以被添加到其中,它通过名字声明了这个模块依赖的一些其他模块,在编译期和运行期都依赖的。

module com.foo.bar {
requires org.baz.qux;
}

最后,exports项可以被添加,它可以仅仅使指定包(package)中的公共类型可以被其他的模块使用。

module com.foo.bar {
requires org.baz.qux;
exports com.foo.bar.alpha;
exports com.foo.bar.beta;
}

如果一个模块的声明中没有exports项,则它根本不向其他模块输出任何的类型。

约定

  • 按照约定,模块声明的源代码被放在了模块源文件结构的根目录,文件的名字叫module-info.java。例如:模块com.foo.bar包含的文件如下:
module-info.java
com/foo/bar/alpha/AlphaFactory.java
  • 按照约定,模块声明被编译到module-info.class文件中,并输出到类文件的输出目录。

模块的名字,像包的名字一样,必须不能重复。命名模块的推荐方式是使用反转域名的方式,它长期被推荐使用到包的命名。模块的名字经常是它的输出包的前缀,但是这个关系也不是强制的。模块的声明既不包括版本号,也不包括它依赖模块的版本号。这是有意这样的:解决版本选择问题不是模块化系统的目的,这个问题最好留个构建工具和容器应用。
模块声明是java程序语言的一部分,而不是他们自己的一个语言或标记,有几个原因:其中最重要的一个原因是模块的信息在编译期和运行期都可用,确保在编译期和运行期以相同的方式运行。这样可以防止很多种错误,至少在编译期提前报告,并且可以更早的诊断和修复。
在一个源文件中表达模块声明,它可以连同模块中的其他文件一起编译,编译成的类文件可以被java虚拟机消费。这种方式对于开发者来说非常熟悉,IDE和构建工具也不难支持。

模块的零件

存在的工具已经可以创建,处理,消费jar文件,为了采用和迁移简单,我们定义了模块jar文件。一个模块jar文件非常像一个普通的jar文件,除了在根目录包含了一个module-info.class。例如上面的com.foo.bar模块jar文件包含以下的内容

META-INF/
MATA-INF/MANIFEST.MF
module-info.java
com/foo/bar/alpha/AlphaFactory.java
  • 模块jar文件可以作为模块使用,在这种情况下,module-info.class包含了模块的声明它可以放在普通的类路径下,这种情况下,module-info.class将被忽略。模块jar文件允许类库的维护者装载一个单一的零件,它可以作为一个模块工作(在java9以后)也可以作为一个普通的jar文件工作。我们希望java9的实践者提升jar工具,使得它更容易的生成模块jar文件。

  • 为了模块化java平台的相关实现,我们介绍了一个新的零件格式,它超越了jar文件,容纳了本地代码、配置文件、和其他的不能自适应的数据类型。这种格式促使模块声明表达式的另外一个优点,把它们编译到class文件中,这个class文件是独立的,这种新的格式,暂时命名为“JMOD”,它被标准化是一个公开的议题

模块描述

编译模块声明到一个类文件的优点是这个类文件有了一个精确定义和可扩展的格式,我们认为module-info.class,它包含了代码级别的编译模式,里边插入的其他变量在初始化时也会被编译。

一个IDE或者打包工具可以插入一些包含标记信息的变量,例如:模块的版本、标题、描述、和许可等。这些信息在编译期和运行期都会被模块系统映射成可使用的信息。它也可以被下游工具构建时使用。指定的变量的集合将被标准化,但是其他的工具和框架也可以定义额外的需要的变量。没有标准化的变量在模块系统中是没有效果的。

平台模块

java9将使用模块化系统将平台分割成若干个子模块。java9平台的实现者可以包含其中的所有模块,也可以是其中的一些。模块系统中明确知道的模块是基础模块,它被命名为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的数据库连接,java.xml处理xml文件,java.log处理日志,java9没有定义的,将会通过“jdk.”的前缀分享出来。

模块的路径

假设我们有一个应用,它使用了上一章讲到的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模块计算出的模块图包含如下的节点和边界

java9模块路径

特性

可读性

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

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

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

可接入性

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

S模块读取(requires)T模块;
T模块输出(exports)T包;

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

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

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

java9可接入性

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

隐性可读性

  • 如果一个模块读取另一个,在某种情况下,它在逻辑上也读取其他的一些模块。举个例子,平台模块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模块。如图:

java9隐性可读性

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

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