Java 9 揭秘(2. 模块化系统)
文 by / 林本托
Tips
做一个终身学习的人。
在此章节中,主要介绍以下内容:
- 在JDK 9之前Java源代码用于编写,打包和部署的方式以及该方法的潜在问题
- JDK 9中有哪些模块
- 如何声明模块及其依赖关系
- 如何封装模块
- 什么是模块路径
- 什么是可观察的模块
- 如何打印可观察模块的列表
- 如何打印模块的描述
本章旨在为你简要概述JDK 9中引入的模块系统。后续章节将详细介绍所有这些概念,并附有实例。 不要担心,如果你第一次不了解所有模块相关的概念。 一旦你获得开发模块代码的经验,你可以回来并重新阅读本章。
一. Java 9 之前的开发
在 JDK 9之前,开发一个 Java 应用程序通常包括以下步骤:
- Java源代码以Java类型(如类,接口,枚举和注释)的形式编写。
- 不同的Java类型被安排在一个包(package)中,而且始终属于一个明确或默认的包。 一个包是一个逻辑的类型集合,本质上为它包含的类型提供一个命名空间。 即使声明为public,包可能包含公共类型,私有类型和一些内部实现类型。
- 编译的代码被打包成一个或多个JAR文件,也称为应用程序JAR,因为它们包含应用程序代码。 一个程序包中的代码可能会引用多个JAR。
- 应用程序可能使用类库。 类库作为一个或多个JAR文件提供给应用程序使用。
- 通过将所有JAR文件,应用程序JAR文件和JAR类库放在类路径上来部署应用程序。
下图显示了JAR文件中打包的代码的典型布局。 该图仅显示了包和Java 类型,不包括其他内容,如manifest.mf文件和资源文件。
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中的代码,没有任何限制,除了在访问控制方面由类型声明指定的代码。
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代码,模块可以看做零个或多个包的集合。 下图显示了三个名为policy
,claim
和utility
的模块,其中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指的是两个模块。
下图描述了两个名为policy
和claim
的模块之间的依赖关系。 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
选项指定要运行的模块及其主类。
继续介绍policy
和claim
模块的例子,假设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.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模块中的内容
在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 -h
和java -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-pat
h或-p
选项指定这些工具的模块路径。
JDK 9引入了与工具一起使用的GNU风格选项。 选项以两个破折号开头,每个单词用短划线分隔,例如--module-path
,--class-path
,--list-modules
等。如果选项接受一个值,则该值可以跟随选项加上空格或=。 以下两个选项是一样的:
- --module-path C:\lib
- --module-path=C:\lib
模块系统在某个阶段(编译时,运行时,工具等)中可用的模块列表被称为可观察模块。 可以使用--list-modules
选项与java命令列出运行时可用的可观察模块。 还可以使用此选项打印模块的描述。