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

by admin on 2020年4月30日

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

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,以便非自动模块可以用常规的方式依赖它:

图片 1

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

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

图片 2

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

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

图片 3

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

图片 4

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

图片 5

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

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

本章旨在为你简要概述JDK
9中引入的模块系统。后续章节将详细介绍所有这些概念,并附有实例。
不要担心,如果你第一次不了解所有模块相关的概念。
一旦你获得开发模块代码的经验,你可以回来并重新阅读本章。

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

2. 使用模块

模块路径.解析.可读性.可访问性.隐式可读性

图片 6

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

1.3 模块描述符

将模块声明编译到类文件中还有一个最终的优势,就是类文件已经具备一种精确定义且可拓展的格式。因此在一个更广泛的意义上,我们可以将module-info.class作为模块描述符,其中包括源码级别的模块声明的编译格式,以及声明被初始编译后插入到类文件属性中的各种其他的信息。

例如,IDE或者构建期的打包工具可以将包含像模块版本,标题,描述以及许可证等文档信息插入到属性中。可以通过模块系统的反射工具在编译期以及运行时来读取这些信息用来生成文档,诊断以及调试等。下游工具也可以使用它来构建特定于操作系统的程序包工件。将会有一系列特定的属性被标准化,但是,既然Java的类文件格式是可拓展的,那么其他的工具或者框架在必要的时候同样可以定义自己的属性。非标准的属性对模块系统本身不会造成影响。

八. 打包模块

模块的artifact可以存储在:

  • 目录中
  • 模块化的JAR文件中
  • JMOD文件中,它是JDK 9中引入的一种新的模块封装格式

稿源:infoQ

4. 服务

七. 模块描述符

在了解上一节中如何声明模块之后,你可能会对模块声明的源代码有几个疑问:

  • 在哪里保存模块声明的源代码? 是否保存在文件中?
    如果是,文件名是什么?
  • 在哪里放置模块声明源代码文件?
  • 模块的声明的源代码如何编译?

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

3.2 由下而上的迁移

把从类路径中加载的类型当作未命名模块的成员允许我们以增量,自下而上的方式将现有应用程序的组件从JAR文件迁移到模块。

假设,上面提到的应用最初是在Java SE
8下构建的,作为放在类路径下的一组类似命名的JAR文件。如果我们按原样在Java
SE
9中运行它们,那么这些JAR文件中的所有类型都会被定义到未命名模块中。该模块可以读取所有其他模块,包括所有内置的平台模块,简单起见,假设只讨论前面提到的java.sql,java.xml,java.logging,以及java.base模块,因为我们得到如下模块图:

图片 7

我们可以直接将org-bar-qux.jar转化为命名模块,因为我们知道它不会引用其它两个JAR文件中的任何类型,所以,作为一个命名模块,它不会引用任何被留在未命名模块中的类型。(我们碰巧从最初的例子中知道这点,但是如果我们还不知道,那么我们可以使用jdeps)我们写一个org.baz.qux的模块声明,并把它添加到模块的源码中,然后编译它,并将结果打包成一个模块JAR文件。如果我们把这个JAR文件放到模块路径中,并且把其它的留在类路径中,我们会得到如下增强的模块图:

图片 8

com-foo-bar.jar和com-foo-app.jar中的代码可以继续工作,因为未命名模块可以读取任意命名模块,包括新的org.baz.qux模块。

我们可以用相似的方式处理com-foo-bar.jar以及com-foo-app.jar,最终重新绘制前面显示的模块图:

图片 9

我们知道对原始的JAR文件中的类做了什么,当然可以一步就将三个应用模块化。然而,如果org-baz-qux.jar是由一个完全不同的团队或者组织单独维护的,那么它可以在其他两个组件之前被模块化,同样com-foo-bar.jar可以在com-foo-app.jar之前被模块化。

1. 编译模块声明

模块声明存储在名为module-info.java的文件中,该文件存储在该模块的源文件层次结构的根目录下。
Java编译器将模块声明编译为名为module-info.class的文件。
module-info.class文件被称为模块描述符,它被放置在模块的编译代码层次结构的根目录下。
如果将模块的编译代码打包到JAR文件中,则module-info.class文件将存储在JAR文件的根目录下。

模块声明不包含可执行代码。 实质上,它包含一个模块的配置。
那为什么我们不在XML或JSON格式的文本文件中保留模块声明,而是在类文件中?
类文件被选为模块描述符,因为类文件具有可扩展,明确定义的格式。
模块描述符包含源码级模块声明的编译形式。 它可以通过工具来增强,例如
jar工具,在模块声明初始编译之后,在类文件属性中包含附加信息。
类文件格式还允许开发人员在模块声明中使用导入和注解。

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

5.4 未命名模块

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

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

2. 模块化JAR中的模块

JDK附带一个jar工具,以JAR(Java Archive)文件格式打包Java代码。
JAR格式基于ZIP文件格式。 JDK 9增强了在JAR中打包模块代码的jar工具。
当JAR包含模块的编译代码时,JAR称为模块化JAR。
模块化JAR在根目录下包含一个module-info.class文件。

无论在JDK 9之前使用JAR,现在都可以使用模块化JAR。
例如,模块化JAR可以放置在类路径上,在这种情况下,模块化JAR中的module-info.class文件将被忽略,因为module-info在Java中不是有效的类名。

在打包模块化JAR的同时,可以使用JDK
9中添加的jar工具中可用的各种选项,将模块描述符中的信息例如模块版本添加到主类中。

Tips
模块化JAR在各个方面来看都是一个JAR,除了它在根路径下包含的模块描述符。通常,比较重要的Java应用程序由多个模块组成。
模块化JAR可以是一个模块,包含编译的代码。
需要将应用程序的所有模块打包到单个JAR中。

继续上一节中的示例,com.jdojo.contact模块的模块化JAR内容如下。
请注意,JAR在META-INF目录中始终包含一个MANIFEST.MF文件。

module-info.class
com/jdojo/contact/info/Address.class
com/jdojo/contact/info/Phone.class
com/jdojo/contact/validator/Validator.class
com/jdojo/contact/validator/AddressValidator.class
com/jdojo/contact/validator/PhoneValidator.class
META-INF/MANIFEST.MF

3 兼容性&迁移

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

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

一. Java 9 之前的开发

在 JDK 9之前,开发一个 Java 应用程序通常包括以下步骤:

  • Java源代码以Java类型(如类,接口,枚举和注释)的形式编写。
  • 不同的Java类型被安排在一个包(package)中,而且始终属于一个明确或默认的包。
    一个包是一个逻辑的类型集合,本质上为它包含的类型提供一个命名空间。
    即使声明为public,包可能包含公共类型,私有类型和一些内部实现类型。
  • 编译的代码被打包成一个或多个JAR文件,也称为应用程序JAR,因为它们包含应用程序代码。
    一个程序包中的代码可能会引用多个JAR。
  • 应用程序可能使用类库。 类库作为一个或多个JAR文件提供给应用程序使用。
  • 通过将所有JAR文件,应用程序JAR文件和JAR类库放在类路径上来部署应用程序。

下图显示了JAR文件中打包的代码的典型布局。 该图仅显示了包和Java
类型,不包括其他内容,如manifest.mf文件和资源文件。

图片 10

20多年来,Java社区以这种编写,编译,打包和部署Java代码的方式开发。
但是,20年漫长的旅程并没有像你所希望的一样顺利!
这样安排和运行Java代码就存在固有的问题:

  • 一个包只是一个类型的容器,而不强制执行任何可访问性边界。包中的公共类型可以在所有其他包中访问;没有办法阻止在一个包中公开类型的全局可见性。

  • 除了以java和javax开头的包外,包应该是开放扩展的。如果你在具有包级别访问的JAR中进行了类型化,则可以在其他JAR中访问定义与你的名称相同的包中的类型。

  • Java运行时会看到从JAR列表加载的一组包。没有办法知道是否在不同的JAR中有多个相同类型的副本。Java运行时首先加载在类路径中遇到的JAR中找到的类型。

  • Java运行时可能会出现由于应用程序在类路径中需要的其中一个JAR引起的运行时缺少类型的情况。当代码尝试使用它们时,缺少的类型会引起运行时错误。

  • 在启动时没有办法知道应用程序中使用的某些类型已经丢失。还可以包含错误的JAR文件版本,并在运行时产生错误。

这些问题在Java社区中非常频繁和臭名昭着,他们得到了一个名字
——JAR-hell。
包装JDK和JRE也是一个问题。
它们作为一个整体作为使用,从而增加了下载时间,启动时间和内存占用。
单体JRE使得Java不可能在内存很小的设备上使用。
如果将Java应用程序部署到云端,则需要支付更多的费用购买更多的使用内存。
大多数情况下,单体JRE使用的内存比所需的内存多,这意味着需要为云服务支付更多的内存。
Java
8中引入的Compact配置文件通过允许将JRE的一个子集打包在称为紧凑配置文件的自定义运行时映像中,大大减少了JRE大小,从而减少了运行时内存占用。

Tips
在早期访问版本中,JDK
9包含三个名为java.compact1,java.compact2和java.compact3的模块,这些模块对应于JDK
8中的三个compact配置文件。之后,它们被删除,因为JDK中的模块可以完全控制在自定义JRE中包含的模块列表。

可以将JDK 9之前的JDK/JRE中的这些问题分为三类:

  • 不可靠的配置
  • 弱封装
  • JDK/JRE的单体结构

下图显示了Java运行时如何看到类路径上的所有JAR,以及如何从其他JAR访问一个JAR中的代码,没有任何限制,除了在访问控制方面由类型声明指定的代码。

图片 11

Java 9通过引入开发,打包和部署Java应用程序的新方法来解决这些问题。
在Java 9中,Java应用程序由称为模块的小型交互组件组成。 Java
9也已经将JDK/JRE组织为一组模块。

2.1 模块路径

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

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

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

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

十一. 总结

Java中的包已被用作类型的容器。 应用程序由放置在类路径上的几个JAR组成。
软件包作为类型的容器,不强制执行任何可访问性边界。
类型的可访问性内置在使用修饰符的类型声明中。
如果包中包含内部实现,则无法阻止程序的其他部分访问内部实现。
类路径机制在使用类型时线性搜索类型。
这导致在部署的JAR中缺少类型时,在运行时接收错误的另一个问题 ——
有时在部署应用程序后很长时间。 这些问题可以分为两种类型:封装和配置。

JDK 9引入了模块系统。 它提供了一种组织Java程序的方法。
它有两个主要目标:强大的封装和可靠的配置。
使用模块系统,应用程序由模块组成,这些模块被命名为代码和数据的集合。
模块通过其声明来控制模块的其他模块可以访问的部分。
访问另一个模块的部分的模块必须声明对第二个模块的依赖。
控制访问和声明依赖的是达成强封装的基础。
在应用程序启动时解决了一个模块的依赖关系。 在JDK
9中,如果一个模块依赖于另一个模块,并且运行应用程序时第二个模块丢失,则在启动时将会收到一个错误,而不是应用程序运行后的某个时间。
这是一个可靠的基础配置。

使用模块声明定义模块。
模块的源代码通常存储在名为module-info.java的文件中。
一个模块被编译成一个类文件,通常命名为module-info.class。
编译后的模块声明称为模块描述符。 模块声明不允许指定模块版本。
但诸如将模块打包到JAR中的jar工具的可以将模块版本添加到模块描述符中。

使用module关键字声明模块,后跟模块名称。
模块声明可以使用五种类型的模块语句:exports,opens,require,uses和provide。
导出语句将模块的指定包导出到所有模块或编译时和运行时的命名模块列表。
开放语句允许对所有模块的反射访问指定的包或运行时指定的模块列表,
其他模块可以使用反射访问指定包中的所有类型以及这些类型的所有成员(私有和公共)。
使用语句和提供模块语句用于配置模块以发现服务实现并提供特定服务接口的服务实现。

从JDK 9开始,open, module, requires, transitive,
exports,opens,to,uses,provides和with都是受限关键字。
只有当具体位置出现在模块声明中时,它们才具有特殊意义。

模块的源代码和编译代码被安排在目录,JAR文件或JMOD文件中。
在目录和JAR文件中,module-info.class文件位于根目录。

与类路径类似,JDK 9引入了模块路径。 但是,它们的使用方式有所不同。
类路径用于搜索类型的定义,而模块路径用于查找模块,而不是模块中的特定类型。
Java工具(如java和javac)已经被更新为使用模块路径和类路径。
您可以使用--module-path或-p选项指定这些工具的模块路径。

JDK 9引入了与工具一起使用的GNU风格选项。
选项以两个破折号开头,每个单词用短划线分隔,例如--module-path--class-path--list-modules等。如果选项接受一个值,则该值可以跟随选项加上空格或=。
以下两个选项是一样的:

  • –module-path C:lib
  • –module-path=C:lib

模块系统在某个阶段(编译时,运行时,工具等)中可用的模块列表被称为可观察模块。
可以使用--list-modules选项与java命令列出运行时可用的可观察模块。
还可以使用此选项打印模块的描述。

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类必须可以通过反射实例化该驱动类;为了实现这一点,模块系统必须将驱动模块添加到模块图中并解决它的依赖,因此:

图片 12

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

模块系统可以通过扫描模块工件中的类文件来调用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语句。自动模块可以使用任意的可用服务。

1. 模块图

模块系统只知道一个模块:java.base。 java.base模块不依赖于任何其他模块。
所有其他模块都隐含地依赖于java.base模块。
应用程序的模块化结构可以被视为一个称为模块图。
在模块图中,每个模块都表示为一个节点。
如果第一个模块依赖于第二个模块,则存在从模块到另一个模块的有向边。
通过将称为根模块的一组初始模块的依赖关系与称为可观察模块的模块系统已知的一组模块相结合来构建模块图。

Tips
模块解析意味着该模块所依赖的模块可用。
假设名为P的模块取决于两个名为Q和R的模块。解析模块P表示您定位模块Q和R,并递归地解析模块Q和R。

构建模块图旨在在编译时,链接时和运行时解析模块依赖关系。
模块解析从根模块开始,并遵循依赖关系链接,直到达到java.base模块。
有时,可能在模块路径上有一个模块,但是会收到该模块未找到的错误。
如果模块未解析,并且未包含在模块图中,则可能会发生这种情况。
对于要解决的模块,需要从根模块开始依赖关系链。
根据调用编译器或Java启动器的方式,选择一组默认的根模块。
还可以将模块添加到默认的根模块中。
了解在不同情况下如何选择默认根模块很重要:

  • 如果应用程序代码是从类路径编译的,或者主类是从类路径运行的,则默认的根模块将由java.se模块和所有非“java.”系统模块组成,如“jdk.”和“JavaFX.”。
    如果java.se模块不存在,则默认的根模块将由所有“java.
    ”和非“java.*”模块组成。
  • 如果您的应用程序由模块组成,则默认的根模块将依赖于以下阶段:
  • 在编译时,它由所有正在编译的模块组成。
  • 在链接时,它是空的。
  • 在运行时,它包含有主类的模块。
    在java命令中使用--module-m选项指定要运行的模块及其主类。

继续介绍policyclaim模块的例子,假设pkg3.Main是claim模块中的主类,并且两个模块都作为模块化JAR打包在C: Java9Revealedlib目录中。下图显示了使用以下命令运行应用程序时在运行时构建的模块图:

图片 13

C:Java9Revealed>java -p lib -m claim/pkg3.Main
claim模块包含应用程序的主类。
因此,claim是创建模块图时唯一的根模块。
policy模块需要被解决,因为claim模块依赖于policy模块。
还需要解析java.base模块,因为所有其他模块都依赖于它,这两个模块也是如此。

模块图的复杂性取决于根模块的数量和模块之间的依赖关系。
假设除了依赖于policy模块之外,claim模块还取决于java.sql的平台模块。
claim模块的新声明如下所示:

module policy {
    requires policy;
    requires java.sql;
}

如下图,显示在claim模块中运行pkg3.Main类时将构建的模块图。
请注意,java.xml和java.logging模块也存在于图中,因为java.sql模块依赖于它们。
在图中,claim模块是唯一的根模块。

图片 14

下图显示了java.se的平台模块的最复杂的模块图形之一。
java.se模块的模块声明如下:

图片 15

java.se 模块的定义如下所示:

module java.se {
    requires transitive java.sql;
    requires transitive java.rmi;
    requires transitive java.desktop;
    requires transitive java.security.jgss;
    requires transitive java.security.sasl;
    requires transitive java.management;
    requires transitive java.logging;
    requires transitive java.xml;
    requires transitive java.scripting;
    requires transitive java.compiler;
    requires transitive java.naming;
    requires transitive java.instrument;
    requires transitive java.xml.crypto;
    requires transitive java.prefs;
    requires transitive java.sql.rowset;
    requires java.base;
    requires transitive java.datatransfer;
}

有时,需要将模块添加到默认的根模块中,以便解析添加的模块。
可以在编译时,链接时和运行使用--add-modules命令行选项指定其他根模块:

--add-modules <module-list>

这里的<module-list>是逗号分隔的模块名称列表。

可以使用以下特殊值作为具有特殊含义的--add-modules选项的模块列表:

  • ALL-DEFAULT
  • ALL-SYSTEM
  • ALL-MODULE-PATH

所有三个特殊值在运行时都有效。 只能在编译时使用ALL-MODULE-PATH。

如果使用ALL-DEFAULT作为模块列表,则从应用程序从类路径运行时使用的默认的根模块集将添加到根集中。
这对于作为容器的应用程序是有用的,托管可能需要容器应用程序本身不需要的其他模块的其他应用程序。
这是一种使所有Java
SE模块可用于容器的方法,因此任何托管的应用程序都可能使用到它们。

如果将ALL-SYSTEM用作模块列表,则将所有系统模块添加到根集中。
这对于运行测试时非常有用。

如果使用ALL-MODULE-PATH作为模块列表,则在模块路径上找到的所有模块都将添加到根集中。
这对于诸如Maven这样的工具非常有用,这确保了应用程序需要模块路径上的所有模块。

Tips
即使模块存在于模块路径上,也可能会收到模块未找到的错误。
在这种情况下,需要使用--add-modules命令行选项将缺少的模块添加到默认的根模块中。

JDK
9支持一个有用的非标准命令行选项,它打印描述在构建模块图时用于解析模块的步骤的诊断消息。
选项是-Xdiag:resolver。 以下命令在声明模块中运行pkg3.Main类。
显示部分输出。 在诊断消息的结尾,你会发现一个结果:部分列出了解决模块。

使用命令C:Java9Revealed>java -Xdiag:resolver -p lib -m claim/pkg3.Main,会得到如下输出:

[Resolver] Root module claim located
[Resolver]   (file:///C:/Java9Revealed/lib/claim.jar)
[Resolver] Module java.base located, required by claim
[Resolver]   (jrt:/java.base)
[Resolver] Module policy located, required by claim
[Resolver]   (file:///C:/Java9Revealed/lib/policy.jar)
...
[Resolver] Result:
[Resolver]   claim
[Resolver]   java.base
...
[Resolver]   policy

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模块的深蓝色的边,因为它们是因该模块而隐式可读的:

图片 16

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

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

九. 模块路径

自JDK开始以来,类路径机制查找类型已经存在。
类路径是一系列目录,JAR文件和ZIP文件。
当Java需要在各个阶段(编译时,运行时,工具使用等)中查找类型时,它会使用类路径中的条目来查找类型。

Java 9类型作为模块的一部分存在。
Java需要在不同阶段查找模块,而不是类似于Java 9之前的模块。Java
9引入了一种新的机制来查找模块,它被称为模块路径。

模块路径是包含模块的路径名称序列,其中路径名可以是模块化JAR,JMOD文件或目录的路径。
路径名由特定于平台的路径分隔符分隔,在UNIX平台上为冒号(:),Windows平台上分号(;)。

当路径名称是模块化的JAR或JMOD文件时,很容易理解。
在这种情况下,如果JAR或JMOD文件中的模块描述符包含要查找的模块的模块定义,则会找到该模块。
如果路径名是目录,则存在以下两种情况:

  • 如果类文件存在于根目录,则该目录被认为具有模块定义。
    根目录下的类文件将被解释为模块描述符。
    所有其他文件和子目录将被解释为此一个模块的一部分。
    如果根目录中存在多个类文件,则首先找到的文件被解释为模块描述符。
    经过几次实验,JDK 9似乎以按字母排列的顺序拾取了第一类文件。
    这种存储模块编译代码的方式肯定会让你头疼。
    因此,如果目录在根目录中包含多个类文件,请避免向模块路径添加目录。
  • 如果根目录中不存在类文件,则目录的内容将被不同的解释。
    目录中的每个模块化JAR或JMOD文件被认为是模块定义。
    每个子目录,如果它包含在它的根一个
    module-info.class文件,被认为具有展开目录树格式的模块定义。
    如果一个子目录的根目录不包含一个module-info.class文件,那么它不会被解释为包含一个模块定义。
    请注意,如果子目录包含模块定义,则其名称不必与模块名称相同。
    模块名称是从module-info.class文件中读取的。

以下是Windows上的有效模块路径:

  • C:mods
  • C:modscom.jdojo.contact.jar;C:modscom.jdojo.person.jar
  • C:lib;C:modscom.jdojo.contact.jar;C:modscom.jdojo.person.jar

第一个模块路径包含名为C:mods的目录的路径。
第二个模块路径包含两个模块化JAR——com.jdojo.contact.jar和com.jdojo.person.jar的路径。
第三个模块路径包含三个元素 ——
目录C:lib的路径,以及两个模块化JAR——com.jdojo.contact.jar和com.jdojo.person.jar的路径。
在类似UNIX的平台上显示相当于这些路径:

  • /usr/ksharan/mods
  • /usr/ksharan/mods/com.jdojo.contact.
  • /usr/ksharan/lib:/usr/ksharan/mods/com.jdojo.contact.

避免模​​块路径问题的最佳方法是不要将分解的目录用作模块定义。

有两个目录作为模块路径 ——
一个包含所有应用程序模块化JAR的目录,另一个包含用于外部库的所有模块化JAR的目录。例如,可以使用C:applib

C:extlib作为Windows上的模块路径,其中C:applib目录包含所有应用程序模块化JAR,C:extlib目录包含所有外部库的模块化JAR。

JDK
9已经更新了所有的工具来使用模块路径来查找模块。这些工具提供了指定模块路径的新选项。到JDK
9,已经看到以一个连字符(-)开头的UNIX样式选项,例如-cp-classpath。在JDK
9中有如此多的附加选项,JDK设计人员对于开发人员来说也用完了有意义的短名称的选项。因此,JDK
9开始使用GNU样式选项,其中选项以两个连续的连字符开头,并且单词由连字符分隔。以下是GNU样式命令行选项的几个示例:

  • –class-path
  • –module-path
  • –module-version
  • –main-class
  • –print-module-descriptor

Tips
要打印工具支持的所有标准选项的列表,使用--help-h选项运行该工具,对于所有非标准选项,使用-X选项运行该工具。
例如,java -hjava -X命令将分别打印java命令的标准和非标准选项列表。

JDK
9中的大多数工具(如javac,java和jar)都支持两个选项来在命令行上指定一个模块路径。
它们是-p--module-path
将继续支持现有的UNIX样式选项以实现向后兼容性。
以下两个命令显示如何使用两个选项来指定java工具的模块路径:

// Using the GNU-style option
C:>java --module-path C:applib;C:lib other-args-go-here
// Using the UNIX-style option
C:>java -p C:applib;C:extlib other-args-go-here

当您使用GNU样式选项时,可以使用以下两种形式之一指定该选项的值:


  • –=

上面的命令也可以写成如下形式:

// Using the GNU-style option
C:>java --module-path=C:applib;C:lib other-args-go-here

当使用空格作为名称值分隔符时,需要至少使用一个空格。
您使用=作为分隔符时,不得在其周围包含任何空格。

1.2 模块工件

现有工具可以创建,操作和使用JAR文件,因此为了方便采用和迁移,我们定义了模块化的JAR文件。一个模块化的JAR文件就像普通的JAR文件一样,除了在根目录下还包含一个module-info.class文件。例如,上述com.foo.bar模块的jar文件中的内容可能像下面这样:

META-INF/
META-INF/MANIFEST.MF
module-info.class
com/foo/bar/alpha/AlphaFactory.class
com/foo/bar/alpha/Alpha.class
...

模块化JAR文件可以作为一个模块,在这种情况下,它的module-info.class文件被用来包含模块的声明。或者,可以把它放到普通的类路径中,这种情况下,它的module-info.class文件将被忽略。模块化JAR文件允许库的维护者既可以以工件的形式传输,来作为Java
SE9
或者更高版本中的模块来使用,也可以作为适用于所有版本中类路径下的常规JAR文件。我们期望Java
SE9的实现中包含一个增强的jar工具,可以很容易的创建模块化的JAR文件。

为了模块化Java
SE平台的参考实现JDK,我们将引入一种新的工件格式,以适应本机代码,配置文件以及其他各种天生并不适配JAR文件的数据。这种格式利用了在源文件中定义模块并编译到类文件中的另一个优势,即类文件跟其他任何特定的工件格式无关。这种新格式,暂时被叫做“JMOD”,它的标准名称至今是个悬而未决的问题。

六. 声明模块

本节包含用于声明模块的语法的快速概述。
在以后的章节中更详细地解释每个部分。
如果不明白本节提到的模块,请继续阅读。

使用模块声明来定义模块,是Java编程语言中的新概念。其语法如下:

[open] module <module> {
       <module-statement>;
       <module-statement>;
       ...
}

open修饰符是可选的,它声明一个开放的模块。
一个开放的模块导出所有的包,以便其他模块使用反射访问。
<module>是要定义的模块的名称。 <module-statement>是一个模块语句。
模块声明中可以包含零个或多个模块语句。
如果它存在,它可以是五种类型的语句之一:

  • 导出语句(exports statement);
  • 开放语句(opens statement);
  • 需要语句(requires statement);
  • 使用语句(uses statement);
  • 提供语句(provides statement)。

导出和开放语句用于控制对模块代码的访问。
需要语句用于声明模块对另一个模块的依赖关系。
使用和提供的语句分别用于表达服务消费和服务提供。
以下是名为myModule的模块的模块声明示例:

module myModule {
    // Exports the packages - com.jdojo.util and
    // com.jdojo.util.parser
    exports com.jdojo.util;
    exports com.jdojo.util.parser;
    // Reads the java.sql module
    requires java.sql;
    // Opens com.jdojo.legacy package for reflective access
    opens com.jdojo.legacy;
    // Uses the service interface java.sql.Driver
    uses java.sql.Driver;
    // Provides the com.jdojo.util.parser.FasterCsvParser
    // class as an implementation for the service interface
    // named com.jdojo.util.CsvParser
    provides com.jdojo.util.CsvParser
        with com.jdojo.util.parser.FasterCsvParser;
}

你可以使用模块声明中的open修饰符来创建一个开放模块。
一个开放模块可以将其所有软件包的反射访问授予其他模块。
你不能在open模块中再使用open语句,因为所有程序包都是在open模块中隐式打开的。
以下代码段声明一个名为myLegacyModule的开放模块:

open module myLegacyModule {
    exports com.jdojo.legacy;
    requires java.sql;
}

总结

  • 在JDK 9之前Java源代码用于编写,打包和部署的方式以及该方法的潜在问题
  • JDK 9中有哪些模块
  • 如何声明模块及其依赖关系
  • 如何封装模块
  • 什么是模块路径
  • 什么是可观察的模块
  • 如何打印可观察模块的列表
  • 如何打印模块的描述

3.4 类路径桥接

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

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

图片 17

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

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

3. JMOD文件中的模块

JDK 9引入了一种称为JMOD的新格式来封装模块。 JMOD文件使用.jmod扩展名。
JDK模块被编译成JMOD格式,放在JDK_HOME
 jmods目录中。例如,可以找到一个包含java.base模块内容的java.base.jmod文件。
仅在编译时和链接时才支持JMOD文件。 它们在运行时不受支持。

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.前缀。

1. 目录中的模块

当模块的编译代码存储在目录中时,目录的根目录包含模块描述符(module-info.class文件),子目录是包层次结构的镜像。
继续上一节中的示例,假设将com.jdojo.contact模块的编译代码存储在C:j9rmods com.jdojo.contact目录中。
目录的内容如下:

comjdojocontactinfoAddress.class
comjdojocontactinfoPhone.class
comjdojocontactvalidatorValidator.class
comjdojocontactvalidatorAddressValidator.class
comjdojocontactvalidatorPhoneValidator.class

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

二. 全新的模块系统

Java 9引入了一个称为模块的新的程序组件。
您可以将Java应用程序视为具有明确定义的边界和这些模块之间依赖关系的交互模块的集合。
模块系统的开发具有以下目标:

  • 可靠的配置
  • 强封装
  • 模块化JDK/JRE

这些目标是解决Java 9之前开发和部署Java应用程序所面临的问题。

可靠的配置解决了用于查找类型的容易出错的类路径机制的问题。
模块必须声明对其他模块的显式依赖。
模块系统验证应用程序开发的所有阶段的依赖关系 —— 编译时,链接时和运行时。
假设一个模块声明对另一个模块的依赖,并且第二个模块在启动时丢失。
JVM检测到依赖关系丢失,并在启动时失败。 在Java
9之前,当使用缺少的类型时,这样的应用程序会生成运行时错误(不是在启动时)。

强大的封装解决了类路径上跨JAR的公共类型的可访问性问题。
模块必须明确声明其中哪些公共类型可以被其他模块访问。
除非这些模块明确地使其公共类型可访问,否则模块不能访问另一个模块中的公共类型。
Java 9中的公共类型并不意味着程序的所有部分都可以访问它。
模块系统增加了更精细的可访问性控制。

Tips
Java
9通过允许模块在开发的所有阶段声明明确的依赖关系并验证这些依赖关系来提供可靠的配置。它通过允许模块声明其公共类型可以访问其他模块的软件包来提供强大的封装。

JDK 9通过将其前身的体结构分解成一组称为平台模块的模块来重写。 JDK
9还引入了一个可选的阶段,称为链接时,这可能在编译时和运行时之间发生。
在链接期间,使用一个链接器,它是JDK
9附带的一个名为jlink的工具,用于创建应用程序的自定义运行时映像,其中仅包含应用程序中使用的模块。
这将运行时的大小调整到最佳大小。

5.3 类加载器

每个类型都在某个模块中,并且运行时每个模块都有一个类加载器,但是一个类加载只加载一个模块吗?事实上,模块系统在模块与类加载器之间存在少量限制。一个类加载器可以一个或多个模块中的类型,只要模块间不互相干扰,并且模块中的所有类型必须是由同一个加载器加载的。

这种灵活性对兼容性极其重要,因为这允许我们保留平台现存的内置类加载器的层次结构。bootstrap和extension类加载器依旧存在,并且用来加载平台模块中的类型。application类加载器也存在,并且用来加载模块路径中的类型。

这种灵活性还可以使现有的应用程序更容易地模块化,这些应用程序已经构建了自定义类加载器的复杂层次结构或甚至图,因为可以升级加载器用来加载模块中的类型,而不必改变其委托模式。

文 by / 林本托

5.5 层

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

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

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

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

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

四. 模块依赖关系

包括JDK 8之前的版本,一个包中的公共类型可以被其他包访问,没有任何限制。
换句话说,包没有控制它们包含的类型的可访问性。 JDK
9中的模块系统对类型的可访问性提供了细粒度的控制。

模块之间的可访问性是所使用的模块和使用模块之间的双向协议:模块明确地使其公共类型可供其他模块使用,并且使用这些公共类型的模块明确声明对第一个模块的依赖。
模块中的所有未导出的软件包都是模块的私有的,它们不能在模块之外使用。

将包中的 API
设置为公共供其他模块使用被称之为导出包。如果名为policy的模块将名为pkg1的包设置为公共类型可用于其他模块访问,则说明policy模块导出包pkg1。如果名为claim的模块声明对policy模块的依赖性,则称之为claim模块读取(read)policy模块。这意味着,在claim模块内部可以访问policy模块导出包中的所有公共类型。模块还可以选择性地将包导出到一个或多个命名模块。这种导出成为qualified导出或module-friendly导出。
qualified导出中的包中的公共类型只能访问指定的命名模块。

在模块系统的上下文中,可以互换使用三个术语 ——
需要(require),读取(read)和依赖(depend)。
以下三个语句意思相同:P读取Q,P需要Q,P依赖Q,其中P和Q指的是两个模块。

下图描述了两个名为policyclaim的模块之间的依赖关系。
policy模块包含两个名为pkg1和pkg2的包,它导出包pkg1,该包使用虚线边界显示,以将其与未导出的包pkg2区分开来。
claim模块包含两个件包pkg3和pkg4,它不导出包。
它声明了对policy模块的依赖。

图片 18

在JDK 9中,您可以如下声明这两个模块:

module policy {
    exports pkg1;
}
module claim {
    requires policy;
}

Tips
用于指示模块中的依赖关系的语法是不对称的
——导出一个包,但需要一个模块。

如果你的模块依赖于另一个模块,则该模块声明要求知道模块名称。几个Java框架和工具在很大程度上依赖于反射来在运行时访问未导出的模块的代码。它们提供了很大的功能,如依赖注入,序列化,Java
Persistence
API的实现,代码自动化和调试。Spring,Hibernate和XStream是这样的框架和库的例子。这些框架和库不了解你的应用程序模块。
但是,他们需要访问模块中的类型来完成他们的工作。
他们还需要访问模块的私有成员,这打破了JDK
9中强封装的前提。当模块导出软件包时,依赖于第一个模块的其他模块只能访问导出的软件包中的公共API。
在运行时,在模块的所有软件包上授予深入的反射访问权限(访问公共和私有API),可以声明一个开放的模块。

图片 19

在JDK 9中,可以如下声明这两个模块:

open module policy.model {
    requires jdojo.jpa;
}
module jdojo.jpa {
    // The module exports its packages here
}

1.1 模块声明

一个模块的自描述性很好的体现在了它的模块声明上,一种Java编程语言中定义的新结构。一个可能的最简单的模块定义差不多只需要指定模块的名字即可:

module com.foo.bar { }  

可以定义一个或多个requires语句来指定该模块在编译期以及运行时所依赖的其他模块的名字:

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

最后,exports语句用来声明指定的包中的所有的,并且只有,public类型对其他模块可见:

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
com/foo/bar/alpha/Alpha.java
...

按照约定,模块定义被编译到module-info.class文件中,同样会被放到类文件的输出目录中去。

模块名称,就像包名称,必须不能有冲突。就像我们一直推荐的包取名的模式一样,我们推荐使用翻转域名的模式来给模块取名字。因此,通常模块的名字是导出包的名字的前缀,不过这种关系并不是强制性的。

一个模块的定义不包含版本字符串,也不包含它所依赖的其他模块的版本号,这是有意为之的:解决版本选择问题并不是模块系统的一个目标,我们更倾向于将它留给构建工具或者容器来解决。

由于一些原因,模块声明是Java编程语言的一部分,而不是一门自成体系的语言或符号,其中最重要的就是模块信息在编译期和运行时必须都可用,以达到跨阶段的高保真度,例如,确保模块系统在编译期跟运行时能以相同的方式工作,相应的,这就可以避免一些错误,或者至少在编译期能更早的报告错误,然后诊断并修复。

将源文件中的模块声明与模块中的其他源文件一起转换为Java虚拟机所需的类文件是建立保真度的一种常规手段。这种方式对于开发人员相当熟悉,并且对于IDE或者构建工具的支持也不困难。尤其是IDE,可以根据组件的项目描述中已经提供的信息来合成requires语句,从而为现有组件提供初始模块声明的建议。

4. 配置服务

Java允许使用服务提供者和服务使用者分离的服务提供者机制。 JDK
9允许使用语句(uses statement)和提供语句(provides
statement)实现其服务。

使用语句可以指定服务接口的名字,当前模块就会发现它,使用
java.util.ServiceLoader类进行加载。格式如下:

uses <service-interface>;

使用语句的实例如下:

module M {
    uses com.jdojo.prime.PrimeChecker;
}

com.jdojo.PrimeChecker是一个服务接口,其实现类将由其他模块提供。
模块M将使用java.util.ServiceLoader类来发现和加载此接口的实现。

提供语句指定服务接口的一个或多个服务提供程序实现类。 它采取以下形式:

provides <service-interface>
    with <service-impl-class1>, <service-impl-class2>...;

相同的模块可以提供服务实现,可以发现和加载服务。
模块还可以发现和加载一种服务,并为另一种服务提供实现。 以下是例子:

module P {
    uses com.jdojo.CsvParser;
    provides com.jdojo.CsvParser
        with com.jdojo.CsvParserImpl;
    provides com.jdojo.prime.PrimeChecker
        with com.jdojo.prime.generic.FasterPrimeChecker;
}

5.1 映射

为了使模块图在运行时通过反射可用,我们在java.lang.reflect包中定义了Module类,并在java.lang.module包中新定义了一些相关类型。Module类的实例代表运行时的单个模块。每个类型都在某个模块中,因此每个Class对象都有一个相关联的Module对象,可以通过Class::getModule方法返回。

模块对象的基本操作如下:

package java.lang.reflect;

public final class Module {
    public String getName();
    public ModuleDescriptor getDescriptor();
    public ClassLoader getClassLoader();
    public boolean canRead(Module target);
    public boolean isExported(String packageName);
}

其中ModuleDescriptor是java.lang.module包中的类,它的实例表示模块描述符;getClassLoder方法返回模块的类加载器;canRead方法判断该模块是否可以读取目标模块;以及isExported方法判断指定的包是否被模块导出。

java.lang.reflect包不是平台唯一的反射工具。编译期javax.lang.model包中会有类似的添加,以支持注解处理器以及文档工具。

3. 声明依赖关系

需要(require)语句声明当前模块与另一个模块的依赖关系。
一个名为M的模块中的“需要N”语句表示模块M取决于(或读取)模块N。语句有以下形式:

requires <module>;
requires transitive <module>;
requires static <module>;
requires transitive static <module>;

require语句中的静态修饰符表示在编译时的依赖是强制的,但在运行时是可选的。requires static N语句意味着模块M取决于模块N,模块N必须在编译时出现才能编译模块M,而在运行时存在模块N是可选的。require语句中的transitive修饰符会导致依赖于当前模块的其他模块具有隐式依赖性。假设有三个模块P,Q和R,假设模块Q包含requires transitive R语句,如果如果模块P包含包含requires Q语句,这意味着模块P隐含地取决于模块R。

1 定义模块

为了能够在即对开发者友好,又能支持当前工具链的前提下,提供可靠的配置以及强封装性,我们把模块看作一种全新的Java基础程序组件。一个模块就是一个由代码跟数据组成的有名称的且自描述的集合。代码被组织为一个包含类型,如Java类跟接口的包的集合,而数据则包括资源或其他形式的静态信息。

三. 什么是模块化

模块是代码和数据集合。 它可以包含Java代码和本地代码。
Java代码被组织为一组包含诸如类,接口,枚举和注解等类型的类。
数据可以包括诸如图像文件和配置文件的资源。

对于Java代码,模块可以看做零个或多个包的集合。
下图显示了三个名为policyclaimutility的模块,其中policy模块包含两个包,claim模块包含一个包,而utility模块不包含任何包。

图片 20

一个模块不仅仅是一个包的容器。 除了其名称,模块定义包含以下内容:

  • 所需的其他模块(或依赖于)的列表
  • 导出的软件包列表(其公共API),其他模块可以使用
  • 开放的软件包(其整个API,公共和私有)到其他反射访问模块的列表
  • 使用的服务列表(或使用java.util.ServiceLoader类发现和加载)
  • 提供的服务的实现列表

在使用这些模块时,可以使用这些方面中的一个或多个。

Java SE 9平台规范将平台划分为称为平台模块的一组模块。 Java SE
9平台的实现可能包含一些或所有平台模块,从而提供可扩展的Java运行时。
标准模块的名字是以Java 为前缀。 Java
SE标准模块的示例有java.base,java.sql,java.xml和java.logging。
支持标准平台模块中的API,供开发人员使用。

非标准平台模块是JDK的一部分,但未在Java SE平台规范中指定。
这些JDK特定的模块的名称以jdk为前缀。
JDK特定模块的示例是jdk.charsets,jdk.compiler,jdk.jlink,jdk.policytool和jdk.zipfs。
JDK特定模块中的API不适用于开发人员。
这些API通常用于JDK本身以及不能轻易获得使用Java SE
API所需功能的库开发人员使用。
如果使用这些模块中的API,则可能会在未经通知的情况下对其进行支持或更改。

JavaFX不是Java SE 9平台规范的一部分。
但是,在安装JDK/JRE时,会安装与JavaFX相关的模块。
JavaFX模块名称以javafx为前缀。
JavaFX模块的示例是javafx.base,javafx.controls,javafx.fxml,javafx.graphics和javafx.web。

作为Java SE 9平台的一部分的java.base模块是原始模块。
它不依赖于任何其他模块。 模块系统只知道java.base模块。
它通过模块中指定的依赖关系发现所有其他模块。 java.base模块导出核心Java
SE软件包,如java.lang,java.io,java.math,java.text,java.time,java.util等。

5.6 有限制的导出

有时候有必要重新安排某些类型,使其对于一些模块是可访问的,而对其他的所有模块是不可访问的。

例如,标准JDK实现java.sql跟java.xml模块中的代码使用了定义在内部包sun.reflect中的类型,该包位于java.base模块中。为了使这些代码可以访问sun.reflect包中的类型,我们可以简单在java.base模块中导出该包:

module java.base {
    ...
    exports sun.reflect;
}

然而这使得sun.reflect包中的所有类型对于其它任意模块都是可以访问的了,因为所有模块都可以读取java.base模块,这不是我们想要的结果,因为该包中的一些类定义了授权,安全敏感的方法。

因为我们拓展模块定义来允许包被导出给一个或多个特定名称的模块,而不会导出给除此之外的其他模块。java.base模块的声明事实上只把sun.reflect包导出给了特定的几个JDK模块:

module java.base {
    ...
    exports sun.reflect to
        java.corba,
        java.logging,
        java.sql,
        java.sql.rowset,
        jdk.scripting.nashorn;
}

这种有限制的导出可以在模块图中以另一种类型的边呈现,这里是金色的,从包到它所导出的指定的模块:

图片 21

下面精炼了上面陈述的可访问性规则:两个类型S和T被定义在不同的模块中,并且T是公共的,那么S中的代码可以访问T的条件是:

  1. S的模块可以读取T的模块,并且
  2. T的模块导出T所在的包,到S所在的模块,或者所有模块。

同样我们拓展反射Module类添加一个方法来判断是否某个包被导出给了特定的模块,而不是所有模块:

public final class Module {
    ...
    public boolean isExported(String packageName, Module target);
}

有限制的导出不经意的就会使得内部类型对一些不想对其公开的模块变成可访问的,因此它们必须被小心使用。例如,一些恶意用户可以将模块命名为java.corba来访问sun.reflect包。为了避免这种情况,我们可以在构建期分析一些相关的模块并且在模块描述符中,记录那些被允许依赖它的模块以及使用它的有限制的导出的模块的内容哈希值。解析的时候,对于名字在有限制的导出列表中的模块,我们要验证它的哈希值是否与记录的该名字的模块的哈希值一致。只要以这种方式将模块的声明与使用绑在一起,有限制的导出在不受信的环境下使用也是安全的。

在此章节中,主要介绍以下内容:

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模块的导出包。

2. 模块的访问控制

导出语句将模块的指定包导出到所有模块或编译时和运行时的命名模块列表。
它的两种形式如下:

exports <package>;
exports <package> to <module1>, <module2>...;

以下是使用了导出语句的模块示例:

module M {
    exports com.jdojo.util;
    exports com.jdojo.policy
         to com.jdojo.claim, com.jdojo.billing;
}

开放语句允许对所有模块的反射访问指定的包或运行时指定的模块列表。
其他模块可以使用反射访问指定包中的所有类型以及这些类型的所有成员(私有和公共)。
开放语句采用以下形式:

opens <package>;
opens <package> to <module1>, <module2>...;

使用开放语句的实例:

module M {
    opens com.jdojo.claim.model;
    opens com.jdojo.policy.model to core.hibernate;
    opens com.jdojo.services to core.spring;
}

Tips
对比导出和打开语句。
导出语句允许仅在编译时和运行时访问指定包的公共API,而打开语句允许在运行时使用反射访问指定包中的所有类型的公共和私有成员。

如果模块需要在编译时从另一个模块访问公共类型,并在运行时使用反射访问类型的私有成员,则第二个模块可以导出并打开相同的软件包,如下所示:

module N {
    exports com.jdojo.claim.model;
    opens com.jdojo.claim.model;
}

阅读有关模块的时候会遇到三个短语:

  • 模块M导出包P
  • 模块M打开包Q
  • 模块M包含包R

前两个短语对应于模块中导出语句和开放语句。
第三个短语意味着该模块包含的包R既不导出也不开放。
在模块系统的早期设计中,第三种情况被称为“模块M隐藏包R”。

2.4 可访问性

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

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

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

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

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

图片 22

模块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不依赖它,因此不可读去该模块。

五. 聚合模块

你可以创建一个不包含任何代码的模块。 它收集并重新导出其他模块的内容。
这样的模块称为聚合模块。假设有几个模块依赖于五个模块。
您可以为这五个模块创建一个聚合模块,现在,你的模块只能依赖于一个模块 ——
聚合模块。

为了方便, Java 9包含几个聚合模块,如java.se和java.se.ee。
java.se模块收集Java SE的不与Java EE重叠的部分。
java.se.ee模块收集组成Java SE的所有模块,包括与Java EE重叠的模块。

2.3 可读性</a>

当模块图中的一个模块直接依赖其他模块时,那么第一个模块中的代码就可以引用第二个模块中的类型。因此我们说第一个模块可读取第二个模块,或者等价的说,第二个模块对第一个模块是可读的。因此,在上面的图中,com.foo.app模块可以读取com.foo.bar以及java.sql模块,但是无法读取org.baz.qux,java.xml,以及java.logging模块。java.logging模块对于java.sql模块是可读的,但对其他模块不可读。(根据定义,任何模块对自己是可读的)

模块图中定义的可读性关系是可靠配置的基础:模块系统确保对每一个其他模块依赖的精确匹配,保证模块图是无回路的,每一个模块最多读取一个给定包的模块,以及定义相同包名的模块互不影响。

可靠的配置不仅更可靠,而且更快。当一个模块中的代码引用某个包中的类型时,我们可以确定该包被定义在了该模块或者该模块可读的其他模块中。因此当搜索某个具体的类型时就没有必要搜索多个模块,更不用糟糕到的去搜索整个类路径。

3. 模块源文件结构

我们来看一个组织源代码和一个名为com.jdojo.contact的模块的编译代码的例子。
该模块包含用于处理联系信息的包,例如地址和电话号码。 它包含两个包:

  • com.jdojo.contact.info
  • com.jdojo.contact.validator

com.jdojo.contact.info包中包含两个类 —— Address 和 Phone。
com.jdojo.contact.validator包中包含一个名为Validator的接口和两个名为AddressValidator和PhoneValidator的类。

下图显示了com.jdojo.contact模块中的内容

图片 23

在Java 9中,Java编译器工具javac添加了几个选项。
它允许一次编译一个模块或多个模块。
如果要一次编译多个模块,则必须将每个模块的源代码存储在与模块名称相同的目录下。
即使只有一个模块,也最好遵循此源目录命名约定。

假设你想编译com.jdojo.contact模块的源代码。
可以将其源代码存储在名为C:j9rsrc的目录中,其中包含以下文件:

module-info.java
comjdojocontactinfoAddress.java
comjdojocontactinfoPhone.java
comjdojocontactvalidatorValidator.java
comjdojocontactvalidatorAddressValidator.java
comjdojocontactvalidatorPhoneValidator.java

请注意,需要遵循包层次结构来存储接口和类的源文件。

如果要一次编译多个模块,则必须将源代码目录命名为com.jdojo.contact,这与模块的名称相同。
在这种情况下,可以将模块的源代码存储在名为C:j9rsrc的目录中,其目录如下:

com.jdojo.contactmodule-info.java
com.jdojo.contactcomjdojocontactinfoAddress.java
com.jdojo.contactcomjdojocontactinfoPhone.java
com.jdojo.contactcomjdojocontactvalidatorValidator.java
com.jdojo.contactcomjdojocontactvalidatorAddressValidator.java
com.jdojo.contactcomjdojocontactvalidatorPhoneValidator.java

模块的编译后代码将遵循与之前看到的相同的目录层次结构。

6 总结

这篇文章中描述的模块系统有很多方面,但是大部分开发者只需要使用它们中常规的基础。我们期望大部分开发者在未来几年,能对模块声明,模块化JAR文件,模块路径,可读性,可访问性,未命名模块,自动模块以及模块化服务等这些基本概念都有适当的理解。相反,对于像反射可读性,层,以及有限制的导出等这些高级的特性的要求就相对少些。

2. 模块版本

在模块系统的初始原型中,模块声明还包括模块版本的。
包括模块版本在声明中使模块系统的实现复杂化,所以模块版本从声明中删除。

模块描述符(类文件格式)的可扩展格式被利用来向模块添加版本。
当将模块的编译代码打包到JAR中时,该jar工具提供了一个添加模块版本的选项,最后将其添加到module-info.class文件中。

致谢

这是该文档的第二版,相比最初版,该版本介绍了兼容性与迁移,修改了映射可读性,重新编排文字,已改善叙述流程,并且分了两级以便于导航阅读。

目前的设计中依旧有很多问题,他们的解决方案将会在该文档的未来版本中给予说明。

Tips
做一个终身学习的人。

7 致谢

Alan Bateman, Alex Buckley, Mandy Chung, Jonathan Gibbons, Chris
Hegarty, Karen Kinnear, and Paul Sandoz都对这篇文章做出了很多贡献。

1. 模块命名

模块名称可以是Java限定标识符。
合法标识符是一个或多个由点分隔的标识符,例如policy,com.jdojo.common和com.jdojo.util。
如果模块名称中的任何部分不是有效的Java标识符,则会发生编译时错误。
例如,com.jdojo.common.1.0不是有效的模块名称,因为名称中的1和0不是有效的Java标识符。

与包命名约定类似,使用反向域名模式为模块提供唯一的名称。
使用这个惯例,名为com.jdojo.common的最简单的模块可以声明如下:

module com.jdojo.common {
    // No module statements
}

模块名称不会隐藏具有相同名称的变量,类型和包。
因此,可以拥有一个模块以及具有相同名称的变量,类型或包。
他们使用的上下文将区分哪个名称是指什么样的实体。

在JDK 9中, open, module, requires, transitive, exports, opens, to,
uses, provides 和
with是受限关键字。只有当具体位置出现在模块声明中时,它们才具有特殊意义。
可以将它们用作程序中其他地方的标识符。
例如,以下模块声明是有效的,即使它不使用直观的模块名称:

// Declare a module named module
module module {
    // Module statements go here
}

第一个模块字被解释为一个关键字,第二个是一个模块名称。

你可以在程序中的任何地方声明一个名为module的变量:

String module = "myModule";

1. 定义模块

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

十. 可观察模块

在模块查找过程中,模块系统使用不同类型的模块路径来定位模块。
在模块路径上与系统模块一起发现的一组模块被称为可观察模块。
可以将可观察模块视为模块系统在特定阶段可用的所有模块的集合,例如编译时,链接时和运行时,或可用于工具。

JDK 9为java命令添加了一个名为--list-modules的新选项。
该选项可用于打印两种类型的信息:可观察模块的列表和一个或多个模块的描述。
该选项可以以两种形式使用:

  • –list-modules
  • –list-modules ,…

在第一种形式中,该选项没有跟随任何模块名称。 它打印可观察模块的列表。
在第二种形式中,该选项后面是逗号分隔的模块名称列表,用于打印指定模块的模块描述符。

以下命令打印可观察模块的列表,其中仅包括系统模块:

c:Java9Revealed> java --list-modules
java.base@9-ea
java.se.ee@9-ea
java.sql@9-ea
javafx.base@9-ea
javafx.controls@9-ea
jdk.jshell@9-ea
jdk.unsupported@9-ea
...

上面显示的是输出部分内容。 输出中的每个条目都包含两个部分——
一个模块名称和一个由@符号分隔的版本字符串。
第一部分是模块名称,第二部分是模块的版本字符串。
例如,在java.base@9-ea中,java.base是模块名称,9-ea是版本字符串。
在版本字符串中,数字9表示JDK 9,ea代表早期访问。
运行命令时,你可能会得到不同的版本字符串输出。

现在在C:Java9Revealedlib目录中放置了三个模块化JAR。
如果提供此目录作为java命令的模块路径,这些模块将被包含在可观察模块列表中。以下命令显示了改变指定一个模块路径后,观察到的模块列表。
这里,lib目录是相对路径,C:Java9Revealed是当前目录。

C:Java9Revealed>java --module-path lib --list-modules
claim (file:///C:/Java9Revealed/lib/claim.jar)
policy (file:///C:/Java9Revealed/lib/policy.jar)
java.base@9-ea
java.xml@9-ea
javafx.base@9-ea
jdk.unsupported@9-ea
jdk.zipfs@9-ea
...

注意,对于应用程序模块,--list-modules选项还会打印它们的位置。
当获得意想不到的结果,并且不知道正在使用哪些模块以及哪些位置时,此信息有助于排除故障。

以下命令将com.jdojo.intro模块指定为--list-modules选项的参数,以打印模块的描述:

C:Java9Revealed>java --module-path lib --list-modules claim
module claim (file:///C:/Java9Revealed/lib/claim.jar)
  exports com.jdojo.claim
  requires java.sql (@9-ea)
  requires mandated java.base (@9-ea)
  contains pkg3

输出的第一行包含模块名称和包含该模块的模块化JAR位置。
第二行表示该模块导出com.jdojo.claim模块。
第三行表示该模块需要java.sql模块。
第四行表示模块强制依赖于java.base模块。
回想一下,除了java.base模块之外的每个模块都取决于java.base模块。
除了java.base模块,在每个模块的描述中看到需要强制的java.base模块。
第五行声明该模块包含一个名为pkg3的包,既不导出也不开放。

你还可以使用--list-modules打印系统模块的描述,例如java.base和java.sql。
以下命令打印出java.sql模块的描述。

C:Java9Revealed>java --list-modules java.sql
module java.sql@9-ea
  exports java.sql
  exports javax.sql
  exports javax.transaction.xa
  requires transitive java.xml
  requires mandated java.base
  requires transitive java.logging
  uses java.sql.Driver

目录

2.2 解析

假设我们有一个应用程序使用上面的com.foo.bar模块和平台的java.sql模块。包含应用程序核心的模块声明如下:

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

对于该初始应用模块,模块系统通过定位其它的可观察到的模块来解析requires语句中的依赖,然后再解析这些可观察到的模块的依赖,直到所有模块的所有依赖都得到解析。这种传递闭包计算的结果是形成一个模块图,对于每一个依赖其他模块的模块都包含一个从第一个模块到第二个模块的有向边。

为了构建com.foo.app模块的模块图,模块系统会检查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模块的模块图应该包含以下节点跟有向边:

图片 24

该图中深蓝色的线表示requires语句中表明的显示的依赖关系,而浅蓝色的线表示每个模块对基模块的隐式的依赖。

模块系统的说明

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

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

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

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

5 高级话题

本文档的剩余部分涉及到的高级话题,虽然重要,不过大部分开发人员可能并不感兴趣。

5. 高级话题

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

2 使用模块

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

3. 兼容性&迁移

未命名的模块.由下而上的迁移.自动模块.类路径的桥接

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图