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.reader
和 com.baeldung.writer
。每个模块的 module-info.java
定义其规范,代码分别位于 com/baeldung/reader
和 com/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 执行顺序:
- compile-student-model
- compile-student-service
- compile-student-service-dbimpl
- compile-student-client
- run-student-client
Linux 执行顺序:
- compile-modules
- 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 源码)。解压后可见以 java
、javafx
、jdk.
开头的目录,每个目录代表一个模块:
-
java.*
:JDK 核心模块 -
javafx.*
:JavaFX 模块 -
jdk.*
:JDK 工具模块
所有 JDK 模块和用户模块都隐式依赖 java.base
模块(包含常用 API 如集合、IO、并发等)。JDK 模块依赖关系如下:
通过查看 JDK 模块的 module-info.java
定义,可直观了解模块声明语法。
9. 结语
本文介绍了模块化应用的创建、编译和运行流程,并展示了 JDK 源码的模块化改造。还有更多激动人心的特性(如使用 jlink 创建小型运行时、模块化 JAR 等),我们将在后续文章中深入探讨。
Project Jigsaw 是 Java 的一次重大变革,其开发者生态(特别是工具和库作者)的接受度仍需时间检验。
本文代码可在 GitHub 获取。