模块系统(JPMS)简介
概述
Java 平台模块系统(Java Platform Module System,简称 JPMS)是 Java 9 引入的一套模块化机制。它通过「模块」(module)把包(package)组织成有边界的单元,每个模块显式声明自己依赖谁(requires)以及对外暴露什么(exports),从而解决传统 classpath 下的「类路径地狱」、依赖隐藏和封装不足等问题。学习 JPMS 有助于理解 JDK 9+ 的目录结构、多版本 JAR 以及现代 Java 应用的模块化方式。
版本说明
JPMS 自 JDK 9 起可用。若你仍在使用 JDK 8,可将本文作为进阶阅读;实际模块化项目通常需 JDK 9+。
为什么需要模块系统
在 JDK 9 之前,Java 依赖 classpath:所有 JAR 平铺在一起,存在以下问题:
- 依赖不透明:无法从声明上看出一个 JAR 依赖了哪些其它 JAR,容易缺包或冲突。
- 封装不足:只要在 classpath 上,任何类都可以通过反射等访问其它 JAR 的「内部」API(如
sun.*、com.sun.*),导致实现细节泄露。 - 类路径地狱:同名类、不同版本的类混在一起,难以管理和排查。
模块系统通过 module-info.java 让每个模块显式声明「我依赖谁」「我对外暴露哪些包」,实现强封装和可分析的依赖图。
模块描述符:module-info.java
每个模块在模块根目录下有一个 module-info.java 文件,称为模块描述符。它只包含模块声明,不是普通 Java 类。
基本语法
java
module 模块名 {
// 依赖与导出声明
}- 模块名:通常与包名前缀一致(如
com.example.app),或与项目 artifact 名一致。 - 模块名与包名没有强制绑定,但习惯上模块会包含若干包,并通过
exports暴露其中一部分。
常见指令
| 指令 | 说明 |
|---|---|
requires 模块名 | 声明本模块依赖另一个模块(编译期和运行期都需要)。 |
requires static 模块名 | 可选依赖:编译期需要,运行期可选(若不存在不会报错)。 |
exports 包名 | 将指定包导出给所有其它模块使用。 |
exports 包名 to 模块1, 模块2 | 仅将包导出给指定模块(限定导出)。 |
opens 包名 | 开放包给反射使用(其他模块可反射访问该包内非 public 成员)。 |
opens 包名 to 模块1, 模块2 | 仅对指定模块开放反射。 |
uses 服务接口 | 声明本模块使用某服务接口(SPI)。 |
provides 服务接口 with 实现类 | 声明本模块提供某服务的实现。 |
示例 1:最简单的模块
一个只依赖 JDK 基础模块并导出自己一个包的模块:
java
// module-info.java(位于模块根目录,与 com/ 等包目录同级)
module com.example.hello {
requires java.base; // 默认已依赖,可省略
exports com.example.hello; // 对外暴露 com.example.hello 包
}java.base是 JDK 的根模块,所有模块隐式requires java.base,可写可不写。- 只有被
exports的包里的 public 类型才能被其它模块访问;未 export 的包对外不可见(强封装)。
示例 2:多模块依赖
模块 A 依赖模块 B,并只把部分包导出:
java
// 模块 B:module-info.java
module com.example.utils {
exports com.example.utils;
}
// 模块 A:module-info.java
module com.example.app {
requires com.example.utils; // 依赖 B
exports com.example.app.api; // 只导出 api 包,app.internal 不导出
}其它模块只能使用 com.example.app.api 中的 public 类型;com.example.app.internal 即使为 public 也对其它模块不可见。
示例 3:opens 与反射
若框架(如 Spring、JPA)需要通过反射访问你的类(包括非 public 成员),则需用 opens 开放包,否则在 JDK 9+ 上可能抛出 InaccessibleObjectException。
java
module com.example.web {
requires com.example.app;
requires spring.context;
// 将 com.example.web 包开放给 Spring,以便反射创建 Bean、注入字段等
opens com.example.web to spring.core;
}提示
- 只给需要反射的模块开放相应包,避免
opens到「所有模块」以减小攻击面。 - 若模块仅自己需要反射(如测试),可使用
opens 包名 to 本模块名。
与 classpath 的对比
| 维度 | classpath(传统) | 模块系统(JPMS) |
|---|---|---|
| 依赖 | 隐式,靠「把 JAR 放进 classpath」 | 显式 requires,缺模块会启动失败 |
| 可见性 | 只要在 classpath 上,都可访问 public 类 | 仅 exports 的包中的 public 类型可被其它模块访问 |
| 反射 | 默认可反射任意类 | 需 opens 才能反射访问,否则可能报错 |
| JDK 结构 | rt.jar 等大 JAR | JDK 自身被拆成模块(如 java.sql、java.xml) |
注意事项
注意
- 未命名模块:传统 classpath 上的 JAR 在 JDK 9+ 上会被视为「未命名模块」,它们可以读取所有模块,但被其它命名模块依赖时需注意兼容性。
- 若项目尚未模块化,可继续使用 classpath;待有清晰边界和封装需求时再引入
module-info.java。
注意
- 模块名与包名不要混淆:模块名在
module-info.java里声明;包名是类所在包。一个模块可包含多个包,只导出其中一部分。 - 反射访问未
opens的包在 JDK 9+ 会受限,若使用 Spring、JPA 等,需在相应模块中为框架所在模块做opens。
相关链接
- Lambda 表达式 — 与模块无直接关系,同属 JDK 新特性
- Stream API — 同上
- 反射入门 — 理解为何需要
opens - Oracle: Java 模块系统 — 语言规范中的模块声明