10.1 Jigsaw中的模块

问题

用户希望访问 Java 标准库中的模块,并将自己的代码封装为模块。

方案

学习 Jigsaw 模块的基础知识,以及如何使用经过模块化处理的 JDK。然后等待 Java 9 正式发布,再决定是否进行升级。

讨论

JSR 376 即 Java 平台模块系统(Java Platform Module System,JPMS),它堪称 Java 9 最大也是最富有争议的变革。对 Java 进行模块化处理的工作已开展近 10 年 3,并取得了不同程度的成功,JPMS 就是这些成果的集中体现。

3Jigsaw 项目于 2008 年启动。

模块系统致力于提供“强有力”的封装,虽然这对维护有利,但其副作用也不容小觑。对一门有 20 多年历史且注重保持向后兼容性的语言来说,这种根本性的调整绝非易事。

例如,模块的概念改变了 publicprivate 的性质。如果模块没有导出特定的包,就无法访问包中的类(即便被声明为 public)。与之类似,如果某个类不在导出的包中,就不能通过反射来访问类中的非公共成员。这将影响到基于反射的库和框架(包括流行的 Spring 和 Hibernate),以及 JVM 上几乎所有的非 Java 语言。作为对各方的让步,Java 开发团队建议在 Java 9 中默认允许使用命令行标志 --illegal-access=permit,但在今后的版本中予以禁用。4

4相关信息参见“Proposal: Allow illegal reflective access by default in JDK 9”。

写作本书时(2017 年 6 月),将 JPMS 规范纳入 Java 9 的提案已被否决过一次,但 JSR 376 专家组正在对规范进行修改,以便为再次投票做准备。5 此外,Java 9 的发布日期被推迟到 2017 年 9 月下旬。6

5在 2017 年 6 月 13 日到 6 月 26 日进行的第二次投票中,JPMS 规范获得了一致通过(一票弃权)。

6Java 9 原定于 2016 年 9 月 22 日发布,但经历了两次重大推迟(2017 年 3 月、2017 年 7 月),主要原因在于各方对模块化的争议较大。——译者注

尽管如此,Java 9 可能会包括 Jigsaw 的部分内容,其基本功能也已确定。本范例旨在介绍这些功能,方便读者了解必要的背景信息,以便在 JPMS 规范获批后做好应用准备。

首先需要指出的是,读者不必急着将自己的代码模块化。虽然 Java 库已被模块化,其他依赖库也正在进行模块化处理,但读者不妨等到系统稳定后再对代码进行操作。

模块

除了所谓的未命名模块(unnamed module),Java 9 定义的模块都有一个名称,并通过名为 module-info.java 的文件定义相关的依赖和需要导出的包。此外,在模块可交付的 JAR 文件中包括一个经过编译的 module-info.class 文件。

module-info.java 文件称为模块描述符(module descriptor),它以关键字 module 开头,通过关键字 requiresexports 描述模块的功能。接下来,我们以一个简单的“Hello, World!”程序为例来讨论,程序将使用两个模块以及 JVM。

两个示例模块为 com.oreilly.supplierscom.kousenit.clients。前者提供表示姓名的字符串流,后者将每个姓名以及欢迎消息打印到控制台。

10.1 Jigsaw中的模块 - 图1 “反向 URL”(reversed URL)模式是目前推荐使用的模块命名约定。

对于 Supplier 模块,NamesSupplier 类的源代码如例 10-1 所示。

例 10-1 提供姓名流

  1. package com.oreilly.suppliers;
  2.  
  3. // 导入
  4.  
  5. public class NamesSupplier implements Supplier<Stream<String>> {
  6. private Path namesPath = Paths.get("server/src/main/resources/names.txt");
  7.  
  8. @Override
  9. public Stream<String> get() {
  10. try {
  11. return Files.lines(namesPath);
  12. } catch (IOException e) {
  13. e.printStackTrace();
  14. return null;
  15. }
  16. }
  17. }

请注意,Supplier 模块保存在 IntelliJ 模块中。不过,IntelliJ IDEA 同样使用“模块”一词,但此“模块”非彼“模块”,它表示“服务器”(server),这也是文本文件的路径中出现“server”的原因。

names.txt 文件的内容如下:7

7是时候在本书中使用《巴比伦五号》的示例了——想必空间站也是由各种模块构成的。(《巴比伦五号》是一部在 1994 年到 1998 年期间播出的美国科幻电视连续剧,共 110 集。names.txt 文件中出现的姓名均为剧中角色。——译者注)

  1. Londo
  2. Vir
  3. G'Kar
  4. Na'Toth
  5. Delenn
  6. Lennier
  7. Kosh

对于 Client 模块,Main 类的源代码如例 10-2 所示。

例 10-2 打印姓名

  1. package com.kousenit.clients;
  2.  
  3. // 导入
  4.  
  5. public class Main {
  6. public static void main(String[] args) throws IOException {
  7. NamesSupplier supplier = new NamesSupplier();
  8.  
  9. try (Stream<String> lines = supplier.get()) {
  10. lines.forEach(line -> System.out.printf("Hello, %s!%n", line));
  11. }
  12. }
  13. }

try-with-resources 自动关闭流

例 10-3 显示了定义 Supplier 模块的 module-info.java 文件。

例 10-3 定义 Supplier 模块

  1. module com.oreilly.suppliers {
  2. exports com.oreilly.suppliers;
  3. }

❶ 模块名

❷ 使模块可供其他模块使用

例 10-4 显示了定义 Client 模块的 module-info.java 文件。

例 10-4 定义 Client 模块

  1. module com.kousenit.clients {
  2. requires com.oreilly.suppliers;
  3. }

❶ 模块名

❷ 请求 Supplier 模块

执行例 10-2 的程序,输出如下:

  1. Hello, Vir!
  2. Hello, G'Kar!
  3. Hello, Na'Toth!
  4. Hello, Delenn!
  5. Hello, Lennier!
  6. Hello, Kosh!

定义 Supplier 模块时必须使用 exports 子句,以便 NamesSupplier 类对 Client 模块可见。Client 模块定义中的 requires 子句用于通知程序,Client 模块需要使用 Supplier 模块中的类。

如果希望在 Supplier 模块中记录对服务器的访问,一种方案是利用 java.util.logging 包定义的 Logger 类在 JVM 中添加一个日志记录器,如例 10-5 所示。

例 10-5 为 Supplier 模块添加日志记录

  1. public class NamesSupplier implements Supplier<Stream<String>> {
  2. private Path namesPath = Paths.get("server/src/main/resources/names.txt");
  3. private Logger logger = Logger.getLogger(this.getClass().getName());
  4.  
  5. @Override
  6. public Stream<String> get() {
  7. logger.info("Request for names on " + Instant.now());
  8. try {
  9. return Files.lines(namesPath);
  10. } catch (IOException e) {
  11. e.printStackTrace();
  12. return null;
  13. }
  14. }
  15. }

❶ 创建日志记录器

❷ 使用时间戳记录对服务器的访问

不过,上述代码无法编译。这是因为经过模块化处理后,JVM 目前默认提供的唯一模块是 java.base,但 java.base 并不包括 java.util.logging 包。为使用 Logger 类,需要更新定义 Supplier 模块的 module-info.java 文件,如例 10-6 所示。

例 10-6 定义 Supplier 模块(更新后的 module-info.java 文件)

  1. module com.oreilly.suppliers {
  2. requires java.logging;
  3. exports com.oreilly.suppliers;
  4. }

➊ 除 java.base 模块外,从 JVM 请求 java.logging 模块

JVM 中的每个模块都有各自的 module-info.java 文件。以 java.logging 模块为例,定义它的 module-info.java 文件如例 10-7 所示。

例 10-7 Logging API 的 module-info.java 文件

  1. module java.logging {
  2. exports java.util.logging;
  3. provides jdk.internal.logger.DefaultLoggerFinder with
  4. sun.util.logging.internal.LoggingProviderImpl;
  5. }

上述 module-info.java 文件不仅导出 java.logging 模块,当客户端请求日志记录器时,还会以 LoggingProviderImpl 类的形式提供 SPI(服务提供者接口)DefaultLoggerFinder 的内部实现。

10.1 Jigsaw中的模块 - 图2 Jigsaw 还提供用于处理服务定位器和提供者的机制,详细信息请参考文档。

希望本范例能对读者有所启发,了解模块是如何定义与应用的。

在 JPMS 规范获批之前,JSR 376 专家组还将解决更多与模块有关的问题,不少问题涉及遗留代码的移植。例如,未命名模块和自动模块(automatic module)的代码不属于任何模块,而是位于“模块路径”(module path)以及由现有遗留 JAR 文件所构成的模块中。有关 JPMS 的争议,相当一部分在于如何处理这些问题。

另见

Jigsaw 的开发属于 OpenJDK 项目的一部分,感兴趣的读者可以阅读快速指引(“Project Jigsaw: Module System Quick-Start Guide”)和相关文档(“The State of the Module System”)。