1. 引言

Project Jigsaw 是一个综合性项目,其新特性主要聚焦于两个核心目标:

  • 在 Java 语言中引入模块系统
  • 在 JDK 源码和 Java 运行时中实现该系统

本文将带你深入了解 Jigsaw 项目及其特性,最后通过一个简单的模块化应用案例收尾。

2. 模块化

简单来说,模块化是一种设计原则,帮助我们实现:

  • 组件间的松耦合
  • 组件间明确的契约和依赖关系
  • 通过强封装隐藏实现细节

2.1. 模块化的基本单元

问题来了:模块化的基本单元是什么?在 Java 世界(尤其是 OSGi 生态)中,JAR 文件长期被视为模块化的基本单元。

JAR 文件确实能帮助我们将相关组件打包,但存在明显局限:

  • JAR 间缺乏显式的契约声明和依赖管理
  • JAR 内部元素的封装性较弱

2.2. JAR 地狱

JAR 文件还带来了另一个问题——JAR 地狱。当类路径上存在多个版本的 JAR 时,ClassLoader 会加载最先找到的类,导致不可预期的行为。

另一个痛点是:应用编译通过,但运行时因类路径缺失 JAR 而抛出 ClassNotFoundException,这种"编译成功,运行失败"的情况让人抓狂。

2.3. 新的模块化单元

鉴于 JAR 作为模块化单元的上述局限,Java 语言设计者引入了名为模块(module)的新语言结构。这标志着 Java 模块化系统的全面革新。

3. Project Jigsaw

该项目的核心动机包括:

  • 为语言创建模块系统(由 JEP 261 实现)
  • 将其应用于 JDK 源码(由 JEP 201 实现)
  • 模块化 JDK 库(由 JEP 200 实现)
  • 更新运行时以支持模块化(由 JEP 220 实现)
  • 支持创建包含 JDK 子集的小型运行时(由 JEP 282 实现)

另一重要举措是封装 JDK 内部 API(如 sun.* 包和其他非标准 API)。这些 API 本不打算公开使用,也未被规划为长期维护。但由于它们强大的能力,许多开发者将其用于库、框架和工具开发。目前部分内部 API 已提供替代方案,其余则被迁移到内部模块中。

4. 模块化新工具

  • jdeps:分析代码库对 JDK API 和第三方 JAR 的依赖,并指明 JDK API 所在模块,极大简化模块化改造
  • jdeprscan:检测代码中是否使用了过时 API
  • jlink:通过组合应用模块和 JDK 模块创建定制化小型运行时
  • jmod:处理 jmod 文件(一种新的模块打包格式),支持包含原生代码、配置文件等 JAR 无法容纳的内容

5. 模块系统架构

Java 语言的模块系统将模块作为顶级结构(类似于包)。开发者可将代码组织为模块,并在模块定义文件中声明依赖关系。

模块定义文件名为 module-info.java,包含:

  • 模块名称
  • 对外开放的包
  • 依赖的模块
  • 消费的服务
  • 提供的服务实现

后两项仅在通过 java.util.ServiceLoader 使用服务时需要。典型模块结构如下:

src
 |----com.baeldung.reader
 |     |----module-info.java
 |     |----com
 |          |----baeldung
 |               |----reader
 |                    |----Test.java
 |----com.baeldung.writer
      |----module-info.java
           |----com
                |----baeldung
                     |----writer
                          |----AnotherTest.java

上述结构定义了两个模块:com.baeldung.readercom.baeldung.writer。每个模块的 module-info.java 定义其规范,代码分别位于 com/baeldung/readercom/baeldung/writer 目录。

5.1. 模块定义关键术语

定义模块(即在 module-info.java 中)时需掌握以下术语:

  • **module**:模块定义的关键字,后跟模块名和定义体
  • **requires**:声明依赖的模块,后跟模块名
  • **transitive**:用于 requires 之后,表示依赖传递性(A 依赖 B,B 依赖 C,则 A 自动依赖 C)
  • **exports**:声明对外开放的包,后跟包名
  • **opens**:声明运行时可反射访问的包(Spring/Hibernate 等框架依赖此特性),也可用于模块级别开放整个模块
  • **uses**:声明消费的服务接口,后跟完整类名
  • **provides ... with ...**:声明提供的服务接口及其实现类

6. 简单模块化应用

我们按以下依赖关系图构建模块化应用:

模块依赖图

com.baeldung.student.model 是根模块,定义模型类 com.baeldung.student.model.Student

public class Student {
    private String registrationId;
    //其他字段、getter/setter略
}

通过 module-info.java 开放 com.baeldung.student.model 包:

module com.baeldung.student.model {
    exports com.baeldung.student.model;
}

com.baeldung.student.service 模块提供抽象 CRUD 接口 com.baeldung.student.service.StudentService

public interface StudentService {
    public String create(Student student);
    public Student read(String registrationId);
    public Student update(Student student);
    public String delete(String registrationId);
}

其依赖 com.baeldung.student.model 并开放 com.baeldung.student.service 包:

module com.baeldung.student.service {
    requires transitive com.baeldung.student.model;
    exports com.baeldung.student.service;
}

com.baeldung.student.service.dbimpl 模块提供实现类 com.baeldung.student.service.dbimpl.StudentDbService

public class StudentDbService implements StudentService {

    public String create(Student student) {
        // 数据库创建操作
        return student.getRegistrationId();
    }

    public Student read(String registrationId) {
        // 数据库读取操作
        return new Student();
    }

    public Student update(Student student) {
        // 数据库更新操作
        return student;
    }

    public String delete(String registrationId) {
        // 数据库删除操作
        return registrationId;
    }
}

它直接依赖 com.baeldung.student.service,并通过传递性依赖 com.baeldung.student.model

module com.baeldung.student.service.dbimpl {
    requires transitive com.baeldung.student.service;
    requires java.logging;
    exports com.baeldung.student.service.dbimpl;
}

客户端模块 com.baeldung.student.client 使用服务实现:

public class StudentClient {

    public static void main(String[] args) {
        StudentService service = new StudentDbService();
        service.create(new Student());
        service.read("17SS0001");
        service.update(new Student());
        service.delete("17SS0001");
    }
}

其模块定义:

module com.baeldung.student.client {
    requires com.baeldung.student.service.dbimpl;
}

7. 编译与运行示例

我们为 Windows 和 Unix 平台提供了编译/运行脚本(见 core-java-9 项目)。

Windows 执行顺序

  1. compile-student-model
  2. compile-student-service
  3. compile-student-service-dbimpl
  4. compile-student-client
  5. run-student-client

Linux 执行顺序

  1. compile-modules
  2. run-student-client

脚本中使用了两个关键参数:

  • --module-source-path:指定多模块源码路径
  • --module-path:替代传统 classpath,指定模块发现路径

Java 9 弃用了 classpath 概念,转而使用模块路径。可通过 --module-path 设置模块发现位置;--module-source-path 则用于编译多模块时指定源码路径。

8. 模块系统在 JDK 源码中的应用

每个 JDK 安装包都包含 src.zip(JDK Java API 源码)。解压后可见以 javajavafxjdk. 开头的目录,每个目录代表一个模块:

JDK9 源码结构

  • java.*:JDK 核心模块
  • javafx.*:JavaFX 模块
  • jdk.*:JDK 工具模块

所有 JDK 模块和用户模块都隐式依赖 java.base 模块(包含常用 API 如集合、IO、并发等)。JDK 模块依赖关系如下:

JDK 模块依赖图

通过查看 JDK 模块的 module-info.java 定义,可直观了解模块声明语法。

9. 结语

本文介绍了模块化应用的创建、编译和运行流程,并展示了 JDK 源码的模块化改造。还有更多激动人心的特性(如使用 jlink 创建小型运行时、模块化 JAR 等),我们将在后续文章中深入探讨。

Project Jigsaw 是 Java 的一次重大变革,其开发者生态(特别是工具和库作者)的接受度仍需时间检验。

本文代码可在 GitHub 获取。


原始标题:Introduction to Project Jigsaw