Java Jigsaw中的模块

Java Jigsaw中的模块,用户希望访问 Java 标准库中的模块,并将自己的代码封装为模块,学习 Jigsaw 模块的基础知识,以及如何使用经过模块化处理的 JDK。然后等待 Java 9 正式发布,再决定是否进行升级。

Java Jigsaw中的模块 问题描述

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

Java Jigsaw中的模块 解决方案

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

Java Jigsaw中的模块 具体实例

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。前者提供表示姓名的字符串流,后者将每个姓名以及欢迎消息打印到控制台。

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

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

例 10-1 提供姓名流

package com.oreilly.suppliers;

// 导入

public class NamesSupplier implements Supplier<Stream<String>> {
    private Path namesPath = Paths.get("server/src/main/resources/names.txt");

    @Override
    public Stream<String> get() {
        try {
            return Files.lines(namesPath);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

请注意,Supplier 模块保存在 IntelliJ 模块中。不过,IntelliJ IDEA 同样使用“模块”一词,但此“模块”非彼“模块”,它表示“服务器”(server),这也是文本文件的路径中出现“server”的原因。
names.txt 文件的内容如下:7
7是时候在本书中使用《巴比伦五号》的示例了——想必空间站也是由各种模块构成的。(《巴比伦五号》是一部在 1994 年到 1998 年期间播出的美国科幻电视连续剧,共 110 集。names.txt 文件中出现的姓名均为剧中角色。——译者注)

Londo
Vir
G'Kar
Na'Toth
Delenn
Lennier
Kosh

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

例 10-2 打印姓名

package com.kousenit.clients;

// 导入

public class Main {
    public static void main(String[] args) throws IOException {
        NamesSupplier supplier = new NamesSupplier();

        try (Stream<String> lines = supplier.get()) {  ➊
            lines.forEach(line -> System.out.printf("Hello, %s!%n", line));
        }
    }
}

try-with-resources 自动关闭流
例 10-3 显示了定义 Supplier 模块的 module-info.java 文件。

例 10-3 定义 Supplier 模块

module com.oreilly.suppliers {      ➊
    exports com.oreilly.suppliers;  ➋
}

❶ 模块名
❷ 使模块可供其他模块使用
例 10-4 显示了定义 Client 模块的 module-info.java 文件。

例 10-4 定义 Client 模块

module com.kousenit.clients {       ➊
    requires com.oreilly.suppliers; ➋
}

❶ 模块名
❷ 请求 Supplier 模块
执行例 10-2 的程序,输出如下:

Hello, Vir!
Hello, G'Kar!
Hello, Na'Toth!
Hello, Delenn!
Hello, Lennier!
Hello, Kosh!

定义 Supplier 模块时必须使用 exports 子句,以便 NamesSupplier 类对 Client 模块可见。Client 模块定义中的 requires 子句用于通知程序,Client 模块需要使用 Supplier 模块中的类。
如果希望在 Supplier 模块中记录对服务器的访问,一种方案是利用 java.util.logging 包定义的 Logger 类在 JVM 中添加一个日志记录器,如例 10-5 所示。

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

public class NamesSupplier implements Supplier<Stream<String>> {
    private Path namesPath = Paths.get("server/src/main/resources/names.txt");
    private Logger logger = Logger.getLogger(this.getClass().getName()); ➊

    @Override
    public Stream<String> get() {
        logger.info("Request for names on " + Instant.now());            ➋
        try {
            return Files.lines(namesPath);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

❶ 创建日志记录器
❷ 使用时间戳记录对服务器的访问
不过,上述代码无法编译。这是因为经过模块化处理后,JVM 目前默认提供的唯一模块是 java.base,但 java.base 并不包括 java.util.logging 包。为使用 Logger 类,需要更新定义 Supplier 模块的 module-info.java 文件,如例 10-6 所示。

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

module com.oreilly.suppliers {
    requires java.logging;          ➊
    exports com.oreilly.suppliers;
}

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

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

module java.logging {
    exports java.util.logging;
    provides jdk.internal.logger.DefaultLoggerFinder with
        sun.util.logging.internal.LoggingProviderImpl;
}

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

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

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

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程