模块化系统 — Project Jigsaw(JDK 9)
飞翔科技的架构评审会上,白歌打开了一张 PPT,标题是「JDK 9 模块化迁移方案」。
孔蓝皱了皱眉:"我们项目引了 200 多个 jar,改模块化要动多少地方?"
白歌切到下一页:"不需要一步到位。JDK 9 的模块化是渐进式的——类路径的 jar 包可以自动划入
unnamed module,新模块和旧 jar 和平共存。我们要做的就是——" 他指着屏幕上的module-info.java,"给核心模块加上这个文件,把 API 和内部实现隔离开来。"黄俪插嘴:"对我们前端有啥影响?"
"以后你们的公共组件库也可以模块化——" 白歌说,"对外只暴露有限的 API,内部实现随便改。这就是 Project Jigsaw 的核心思想:强封装。"
定义表
| 概念 | 描述 |
|---|---|
| 模块(Module) | 一个命名、自描述的代码和数据集合,通过 module-info.java 声明其依赖和导出 |
| module-info.java | 模块根目录下的模块描述文件,定义模块的 requires(依赖)、exports(导出)等 |
| requires | 声明当前模块依赖的其他模块 |
| exports | 将指定的包导出给其他模块使用(未导出的包在模块外不可访问) |
| requires transitive | 传递依赖——依赖当前模块的模块自动获得传递模块的包访问权 |
| opens | 反射访问权限——未 exports 的包可通过 opens 允许反射访问 |
| uses / provides | ServiceLoader 机制声明——uses 声明使用的服务接口,provides 声明提供的实现 |
| 类路径(Classpath) | JDK 9 之前的加载方式,所有类和 jar 扁平化放置,无封装边界 |
| 模块路径(Module Path) | JDK 9 的新加载方式,模块之间存在清晰的依赖关系和访问控制 |
| 未命名模块(Unnamed Module) | 类路径上的所有 jar 和类自动归入的模块,可访问所有已命名模块导出的包 |
| --add-opens | JVM 参数,运行时开放模块的包供反射访问 |
Mermaid 架构图:类路径 vs 模块路径
module-info.java 语法
3.1 requires — 声明依赖
// ===== module-info.java(服务实现模块)=====
module com.feixiang.service.impl {
// 依赖其他模块
requires com.feixiang.service.api; // 编译 + 运行时都依赖
requires java.sql; // JDK 自带模块举例
}
requires:基本依赖声明,编译和运行时都需要该模块requires static:编译时需要,运行时可选(某些只在特定平台存在的模块)requires transitive:传递依赖——A requires transitive B,任何依赖 A 的模块自动依赖 B
// ===== 传递依赖示例 =====
module com.feixiang.api {
requires transitive com.feixiang.common; // 依赖 api 的模块自动得到 common 中的类
}
3.2 exports — 导出包
// ===== module-info.java(核心工具模块)=====
module com.feixiang.core {
// 导出公开 API 包
exports com.feixiang.core.api; // 给所有模块
exports com.feixiang.core.dto; // 给所有模块
// 限定导出(仅指定模块可访问)
exports com.feixiang.core.internal to com.feixiang.service.impl;
// 内部包不导出——外界完全不可见
// com.feixiang.core.util —— 模块外无法 import
// com.feixiang.core.config —— 模块外无法 import
}
3.3 opens — 反射权限
module com.feixiang.core {
exports com.feixiang.core.api;
// 允许反射访问内部包(框架如 Spring / Hibernate 需要)
opens com.feixiang.core.model; // 所有模块可反射
opens com.feixiang.core.dto to spring.beans; // 仅 Spring 可反射
}
运行时动态开放包:
# 如果模块化代码依赖的库使用了反射,可以运行时加 opens 参数
java --add-opens com.feixiang.core/com.feixiang.core.model=spring.beans -p lib -m main.app/com.feixiang.Main
3.4 uses / provides — ServiceLoader 机制
// ===== 服务接口模块 =====
module com.feixiang.spi {
exports com.feixiang.spi; // 导出 SPI 接口
}
// ===== 服务实现模块 =====
module com.feixiang.spi.impl {
requires com.feixiang.spi;
provides com.feixiang.spi.PaymentService // 提供的实现
with com.feixiang.spi.impl.WechatPayImpl;
}
// ===== 服务消费者模块 =====
module com.feixiang.app {
requires com.feixiang.spi;
uses com.feixiang.spi.PaymentService; // 使用该服务
}
Java 代码中通过 ServiceLoader 加载:
ServiceLoader<PaymentService> loader = ServiceLoader.load(PaymentService.class);
for (PaymentService service : loader) {
System.out.println("发现服务: " + service.getClass().getName());
service.pay(amount);
}
JDK 自带模块一览
JDK 9 将 JDK 自身拆分为约 90 个模块,核心模块包括:
| 模块名 | 描述 |
|---|---|
java.base | 根基模块——所有模块都隐式依赖 java.base,包含 java.lang、java.util 等 |
java.sql | JDBC API |
java.xml | XML 解析 |
java.desktop | AWT / Swing 等桌面 GUI |
java.logging | java.util.logging |
java.net.http | JDK 11 引入的 HttpClient |
# 列出当前 JDK 的全部模块
java --list-modules
# 描述某个模块的信息
java --describe-module java.sql
实战示例
5.1 目录结构
飞翔科技模块化项目/
├── src/
│ ├── module.core/
│ │ ├── module-info.java
│ │ └── com/feixiang/core/
│ │ ├── api/EmployeeService.java
│ │ └── internal/EmployeeRepository.java
│ ├── module.web/
│ │ ├── module-info.java
│ │ └── com/feixiang/web/Application.java
│ └── module.spi.impl/
│ ├── module-info.java
│ └── com/feixiang/spi/impl/WechatPayImpl.java
5.2 各模块的 module-info.java
// ===== module.core =====
module com.feixiang.core {
exports com.feixiang.core.api; // 对外 API
exports com.feixiang.core.dto;
exports com.feixiang.core.internal to com.feixiang.web; // 限定导出
opens com.feixiang.core.model to spring.beans; // 反射权限
}
// ===== module.web =====
module com.feixiang.web {
requires com.feixiang.core; // 依赖核心模块
requires java.net.http; // 依赖 JDK HttpClient
requires spring.boot; // 依赖 Spring Boot
}
// ===== module.spi.impl =====
module com.feixiang.spi.impl {
requires com.feixiang.spi;
provides com.feixiang.spi.PaymentService
with com.feixiang.spi.impl.WechatPayImpl,
com.feixiang.spi.impl.AliPayImpl;
}
5.3 编译与运行
# 编译所有模块(模块路径)
javac -d out --module-source-path src $(find src -name "*.java")
# 运行
java --module-path out -m com.feixiang.web/com.feixiang.web.Application
# 定制运行时镜像(jlink 构建精简 JRE)
jlink --module-path out:$JAVA_HOME/jmods \
--add-modules com.feixiang.web \
--output feixiang-runtime \
--strip-debug --compress=2
5.4 内部 API 无法访问的验证
// 在模块外尝试 import 未导出的包
import com.feixiang.core.internal.EmployeeRepository; // ❌ 编译错误
// 错误信息: package com.feixiang.core.internal is not visible
// (package com.feixiang.core.internal is declared in module com.feixiang.core,
// which does not export it)
模块化对类路径的影响
关键行为总结:
| 场景 | 行为 |
|---|---|
| 类路径上的 jar(unnamed module) | 作为单个模块,能读取所有其他模块,但不能被命名模块通过 requires 引用 |
| 模块路径上的模块 | 严格的依赖声明,未 exports 的包完全不可见 |
| 反射访问未导出的包 | 需要通过 --add-opens 命令行参数显式授权 |
总结
| 维度 | 类路径 | 模块化 |
|---|---|---|
| 封装 | 无——所有包相互可见 | 强封装——仅导出包可访问 |
| 依赖检查 | 运行时才暴露 NoClassDefFoundError | 编译时即可发现缺失依赖 |
| JRE 体积 | rt.jar 约 60MB,全量携带 | jlink 自定义镜像,按需裁剪 |
| 迁移策略 | — | 渐进式——模块路径 + 类路径混合运行 |
| 核心关键字 | — | requires / exports / opens / uses / provides |