Java 9 揭秘(2. 模块化系统)

文 by / 林本托java

Tips
作一个终身学习的人。web

Java 9

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

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

本章旨在为你简要概述JDK 9中引入的模块系统。后续章节将详细介绍全部这些概念,并附有实例。 不要担忧,若是你第一次不了解全部模块相关的概念。 一旦你得到开发模块代码的经验,你能够回来并从新阅读本章。sql

一. Java 9 以前的开发

在 JDK 9以前,开发一个 Java 应用程序一般包括如下步骤:shell

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

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

JAR 内部布局

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

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

  • 除了以java和javax开头的包外,包应该是开放扩展的。若是你在具备包级别访问的JAR中进行了类型化,则能够在其余JAR中访问定义与你的名称相同的包中的类型。
  • Java运行时会看到从JAR列表加载的一组包。没有办法知道是否在不一样的JAR中有多个相同类型的副本。Java运行时首先加载在类路径中遇到的JAR中找到的类型。编程语言

  • Java运行时可能会出现因为应用程序在类路径中须要的其中一个JAR引发的运行时缺乏类型的状况。当代码尝试使用它们时,缺乏的类型会引发运行时错误。ide

  • 在启动时没有办法知道应用程序中使用的某些类型已经丢失。还能够包含错误的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中的代码,没有任何限制,除了在访问控制方面由类型声明指定的代码。

类路径上的JAR,由Java运行时加载和访问

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

二. 全新的模块系统

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

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

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

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

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

Tips
Java 9经过容许模块在开发的全部阶段声明明确的依赖关系并验证这些依赖关系来提供可靠的配置。它经过容许模块声明其公共类型能够访问其余模块的软件包来提供强大的封装。

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

三. 什么是模块化

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

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

类型,包和模块的排列

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

  • 所需的其余模块(或依赖于)的列表
  • 导出的软件包列表(其公共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等。

四. 模块依赖关系

包括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模块的依赖。

在两个模块间声明依赖

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

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

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

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

开放的模块容许反射访问其全部成员

在JDK 9中,能够以下声明这两个模块:

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

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:\ Java9Revealed\lib目录中。下图显示了使用如下命令运行应用程序时在运行时构建的模块图:

模块图的示例

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模块是惟一的根模块。

显示对java.sql模块依赖的模块图

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

以java.se为根模块的模块图

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

五. 聚合模块

你能够建立一个不包含任何代码的模块。 它收集并从新导出其余模块的内容。 这样的模块称为聚合模块。假设有几个模块依赖于五个模块。 您能够为这五个模块建立一个聚合模块,如今,你的模块只能依赖于一个模块 —— 聚合模块。

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

六. 声明模块

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

使用模块声明来定义模块,是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;
}

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";

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”。

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。

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;
}

七. 模块描述符

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

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

1. 编译模块声明

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

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

2. 模块版本

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

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

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模块中的内容

com.jdojo.contact模块中的内容

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

假设你想编译com.jdojo.contact模块的源代码。 能够将其源代码存储在名为C:\j9r\src的目录中,其中包含如下文件:

module-info.java
com\jdojo\contact\info\Address.java
com\jdojo\contact\info\Phone.java
com\jdojo\contact\validator\Validator.java
com\jdojo\contact\validator\AddressValidator.java
com\jdojo\contact\validator\PhoneValidator.java

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

若是要一次编译多个模块,则必须将源代码目录命名为com.jdojo.contact,这与模块的名称相同。 在这种状况下,能够将模块的源代码存储在名为C:\j9r\src的目录中,其目录以下:

com.jdojo.contact\module-info.java
com.jdojo.contact\com\jdojo\contact\info\Address.java
com.jdojo.contact\com\jdojo\contact\info\Phone.java
com.jdojo.contact\com\jdojo\contact\validator\Validator.java
com.jdojo.contact\com\jdojo\contact\validator\AddressValidator.java
com.jdojo.contact\com\jdojo\contact\validator\PhoneValidator.java

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

八. 打包模块

模块的artifact能够存储在:

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

1. 目录中的模块

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

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

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. JMOD文件中的模块

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

九. 模块路径

自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:\mods\com.jdojo.contact.jar;C:\mods\com.jdojo.person.jar
  • C:\lib;C:\mods\com.jdojo.contact.jar;C:\mods\com.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.jar:/usr/ksharan/com.jdojo.person.jar
  • /usr/ksharan/lib:/usr/ksharan/mods/com.jdojo.contact.jar:/usr/ksharan/mods/com.jdojo.person.jar

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

有两个目录做为模块路径 —— 一个包含全部应用程序模块化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

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

十. 可观察模块

在模块查找过程当中,模块系统使用不一样类型的模块路径来定位模块。 在模块路径上与系统模块一块儿发现的一组模块被称为可观察模块。 能够将可观察模块视为模块系统在特定阶段可用的全部模块的集合,例如编译时,连接时和运行时,或可用于工具。

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:\Java9Revealed\lib目录中放置了三个模块化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

十一. 总结

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命令列出运行时可用的可观察模块。 还可使用此选项打印模块的描述。