Java 9 的模块系统(JPMS)是什么?module-info.java 解决了什么问题?
简化版
Java 9 引入的模块系统(JPMS,Java Platform Module System,项目代号 Jigsaw)是对「Java 代码组织方式」的一次大升级——在「包(package)」之上加了一层「模块(module)」,用 module-info.java 文件显式声明「这个模块依赖哪些模块、对外暴露哪些包」。它解决的核心问题:① 强封装——过去一个 public 类,任何人都能访问(哪怕它是内部实现);模块化后,一个包只有被 exports 导出才能被别的模块访问,没导出的包是真正「模块私有」的(连反射默认也访问不了),实现了真正的封装;② 显式依赖——requires 声明依赖哪些模块,编译/启动时就检查依赖是否齐全(可靠配置,避免运行时才发现缺类的 ClassNotFoundException);③ 拆分庞大的 JDK——把 JDK 拆成几十个模块(java.base、java.sql 等),可以用 jlink 裁剪出「只含用到的模块」的精简运行时(减小体积)。现实:JPMS 很强大但应用层普及度低(大多数业务项目没用 module-info,还是用 classpath),主要是 JDK 自身和一些库在用。
详细版
module-info.java 的核心指令:
| 指令 | 作用 |
|---|---|
module xxx { } | 声明一个模块 |
requires M | 依赖模块 M |
requires transitive M | 依赖 M 且传递(依赖我的也自动依赖 M) |
exports P | 导出包 P(对外可访问) |
exports P to M | 只对模块 M 导出包 P |
opens P | 开放包 P 给反射(深度反射访问) |
uses S / provides S with I | SPI 服务的使用/提供 |
// module-info.java(放在模块根目录)
module com.example.myapp {
requires java.sql; // 依赖 java.sql 模块
requires transitive com.example.api; // 传递依赖
exports com.example.myapp.api; // 导出这个包(别人能用)
// com.example.myapp.internal 没 exports → 模块私有,别人访问不了
opens com.example.myapp.model; // 开放给反射(如 JSON 序列化框架)
provides com.example.Service // 提供 SPI 服务
with com.example.myapp.ServiceImpl;
}
classpath(传统) vs module-path(模块化):
classpath:所有 jar 平铺,一个 public 类全世界可见,无强封装
module-path:模块边界清晰,只有 exports 的包可见,requires 声明依赖
⚠️ JPMS 的核心理念是「强封装 + 显式依赖」,但它「叫好不叫座」——概念先进却在应用层普及率很低。原因:① 迁移成本高——给已有大项目加
module-info要理清所有依赖和导出,很麻烦;很多老库没模块化,用它们要靠「自动模块(automatic module)」过渡,体验割裂;② 破坏了大量依赖反射「深度访问」的框架——过去框架(Spring、Hibernate、各种序列化库)靠反射访问私有成员,模块化后没opens就访问不了(InaccessibleObjectException),要么加opens、要么加启动参数--add-opens。所以现实是:JDK 自身彻底模块化了,但绝大多数业务应用还在用 classpath(不写 module-info),享受不到强封装,但也避开了迁移的麻烦。面试知道「JPMS 是什么、解决什么、为什么普及慢」即可。
完整版教学
一、问题:Java 9 前的代码组织缺陷
先理解 JPMS 要解决的历史问题:
Java 9 之前的代码组织:
类 → 包(package)→ jar → classpath
一堆 jar 平铺在 classpath 上
三个缺陷:
① 没有真正的封装:
public 类全世界可见——即使是"内部实现"的类,
只要是 public,任何代码都能访问(拦不住)
→ 内部实现被滥用、无法真正隐藏
② 依赖不明确(JAR Hell):
classpath 上一堆 jar,谁依赖谁不清楚
缺了某个 jar,编译不报错、运行时才 ClassNotFoundException
版本冲突、重复类,运行时才炸
→ "JAR 地狱"
③ JDK 太庞大、无法裁剪:
整个 JDK 是一个巨大的 rt.jar
哪怕只用一点点,也要带整个 JDK(几百 MB)
→ 无法做精简的运行时
→ 这三个问题促成了 JPMS
JPMS 要解决 Java 9 前的三个缺陷:① 没有真正的封装(public 类全世界可见,内部实现类只要 public 就拦不住被访问);② 依赖不明确(JAR Hell)(classpath 一堆 jar 谁依赖谁不清楚,缺 jar 运行时才 ClassNotFoundException、版本冲突运行时才炸);③ JDK 太庞大无法裁剪(整个 rt.jar 几百 MB、用一点也要带全部)。理解「Java 9 前三缺陷:没真正封装(public 全可见)、依赖不明确(JAR Hell 运行时才发现缺类)、JDK 庞大无法裁剪」,就理解了 JPMS 要解决什么。
二、模块:包之上的一层
JPMS 引入「模块」——在包之上加一层:
新的层次:
类 → 包 → 模块(module)→ ...
模块是什么:
一组相关的包 + 一个 module-info.java(模块描述符)
module-info 声明:
- 这个模块叫什么(module 名)
- 依赖哪些模块(requires)
- 对外导出哪些包(exports)
module-info.java(放在模块根目录):
module com.example.myapp {
requires java.sql; // 我依赖 java.sql
exports com.example.myapp.api; // 我导出这个包
}
模块边界:
模块内部的包,只有 exports 的才能被外部访问
没 exports 的包 = 模块私有(外部看不到、访问不了)
→ 这就是"强封装"
对比包:
包只是命名空间 + 包级访问控制(同包可见)
模块是更大的封装单元 + 显式依赖声明
JPMS 引入「模块」——在包之上加一层:模块 = 一组相关的包 + module-info.java(模块描述符),module-info 声明模块名、requires(依赖哪些模块)、exports(导出哪些包)。模块边界:模块内只有 exports 的包能被外部访问、没 exports 的包是模块私有(外部看不到)——这就是强封装。对比:包只是命名空间,模块是更大的封装单元 + 显式依赖声明。理解「模块=一组包+module-info(声明模块名/requires 依赖/exports 导出)、只有 exports 的包外部可见没导出的是模块私有(强封装)、模块是包之上的封装单元」,就理解了模块的概念。
三、强封装:exports 与 opens
「强封装」是 JPMS 的核心价值——exports 和 opens 控制访问:
exports P:导出包 P(编译期 + 运行期的普通访问)
别的模块能访问 P 里的 public 类型
没 exports 的包 → 外部完全访问不了(连 public 类都不行)
→ 实现真正的封装:
模块作者能明确"哪些是 API(exports)、哪些是内部实现(不 exports)"
内部实现包不导出 → 外部想用也用不了 → 内部实现可以自由改
exports P to M:定向导出
只对指定模块 M 导出 P(其他模块还是访问不了)
→ 更精细的控制(只给信任的模块用)
opens P:开放包给反射
普通 exports 允许"编译期访问 public 成员"
但反射的"深度访问"(访问 private 成员、setAccessible)需要 opens
opens P → 允许对 P 里的类型做深度反射(框架序列化/注入常需要)
没 opens → 反射深度访问抛 InaccessibleObjectException
opens P to M:定向开放
区别:
exports:编译 + 运行期的普通(public)访问
opens:允许运行期的深度反射访问(含 private)
→ 序列化框架(Jackson)、DI 框架(Spring)常需要 opens
强封装靠 exports 和 opens:exports P 导出包 P(别的模块能访问其 public 类型,没 exports 的包外部完全访问不了,实现真正封装——内部实现包不导出就能自由改);exports P to M 定向导出(只给指定模块);opens P 开放包给反射深度访问(exports 只允许编译期访问 public 成员,反射访问 private/setAccessible 需要 opens,否则抛 InaccessibleObjectException——序列化/DI 框架常需要)。理解「exports 导出包(没导出外部访问不了=真封装)、exports to 定向导出、opens 开放反射深度访问(exports 只 public、反射访问 private 需 opens 否则异常、框架常需要)」,就掌握了强封装机制。
四、显式依赖:requires
「显式依赖」靠 requires——编译/启动时就检查:
requires M:声明依赖模块 M
module myapp {
requires java.sql; // 我需要 java.sql
}
→ 编译和启动时,检查 java.sql 模块是否存在
→ 缺了就报错(在启动时,而非运行到某处才 ClassNotFoundException)
好处(可靠配置):
过去:缺 jar 运行时才炸(ClassNotFoundException)
模块化:启动时就检查所有 requires 的模块齐全 → 早失败、可靠
requires transitive M:传递依赖
module api {
requires transitive com.foundation;
}
→ 依赖 api 的模块,自动也能访问 com.foundation
→ "我用的 API 暴露了 com.foundation 的类型,用我的人也需要它"
→ 避免使用者还要手动 requires com.foundation
requires static M:编译期依赖(运行期可选)
→ 编译需要,运行时不强制(如可选功能、注解处理)
java.base:所有模块隐式依赖
java.base 是最核心的模块(Object、String、集合等)
所有模块自动 requires java.base(不用写)
显式依赖靠 requires:requires M 声明依赖模块 M,编译/启动时检查模块是否齐全(缺了启动时报错、而非运行到某处才 ClassNotFoundException——可靠配置、早失败)。requires transitive M 传递依赖(依赖我的模块自动也能访问 M,用于「我的 API 暴露了 M 的类型」)。requires static M 编译期依赖(运行可选)。java.base 所有模块隐式依赖(核心模块含 Object/String/集合,不用写)。理解「requires 声明依赖(编译/启动时检查齐全、缺了早失败可靠配置)、requires transitive 传递依赖、java.base 隐式依赖」,就掌握了显式依赖。
五、JDK 模块化与 jlink
JPMS 的一大成果是「JDK 自身模块化」,配合 jlink 裁剪:
JDK 被拆成几十个模块:
java.base(核心:Object、String、集合、并发...)
java.sql(JDBC)
java.xml、java.desktop(Swing/AWT)、java.logging...
→ 原来一个大 rt.jar,拆成模块化的 JDK
好处:
① 结构清晰——JDK 内部依赖关系明确
② 可裁剪——用 jlink 生成"只含用到的模块"的精简运行时
jlink(模块化带来的能力):
jlink --add-modules java.base,java.sql,com.myapp
--output myruntime
→ 生成一个只含这几个模块的自定义 JRE
→ 体积小很多(不带用不到的模块)
→ 适合容器化部署(镜像更小)、嵌入式
对比过去:
过去要带整个 JDK(几百 MB),哪怕只用一点
现在 jlink 裁剪 → 几十 MB 的精简运行时
所以 JDK 模块化 + jlink 是 JPMS 的重要实际价值:
更小的运行时、更适合云原生/容器
JPMS 的一大成果是「JDK 自身模块化」——JDK 被拆成几十个模块(java.base 核心、java.sql、java.xml 等,原来一个大 rt.jar 拆成模块)。好处:结构清晰 + 可裁剪。配合 jlink 能生成「只含用到的模块」的精简运行时(体积小很多,适合容器化/嵌入式——过去要带整个 JDK 几百 MB、现在裁剪到几十 MB)。这是 JPMS 的重要实际价值(更小运行时、适合云原生)。理解「JDK 拆成几十个模块(java.base 核心/java.sql 等)、jlink 生成只含用到模块的精简运行时(体积小适合容器)、是 JPMS 重要实际价值」,就理解了 JDK 模块化和 jlink。
六、现实:为什么普及慢
理解 JPMS 「叫好不叫座」的现实:
JPMS 概念先进,但应用层普及率低,原因:
① 迁移成本高:
给已有项目加 module-info → 要理清所有依赖、导出
大项目工作量大、容易出错
② 生态未跟上:
很多老库没模块化,用它们要靠"自动模块"(automatic module)
—— 把非模块化 jar 当一个模块,名字从 jar 名推导,体验割裂
③ 破坏了依赖反射的框架:
Spring、Hibernate、Jackson 等靠反射深度访问
模块化后没 opens 就访问不了 → InaccessibleObjectException
要加 opens 或 --add-opens 启动参数 → 麻烦
现实状况:
- JDK 自身:彻底模块化了(内部实现)
- 大多数业务应用:不写 module-info,还是用 classpath
→ 享受不到强封装,但也避开了迁移麻烦
- 一些库/框架:逐步支持模块化(提供 Automatic-Module-Name)
结论:
JPMS 是好设计,但落地阻力大,应用层普及慢
面试知道"是什么、解决什么、为什么普及慢"即可
实际大多数项目还是 classpath
JPMS「叫好不叫座」的原因:① 迁移成本高(给已有项目加 module-info 要理清所有依赖导出、工作量大);② 生态未跟上(很多老库没模块化,用「自动模块」过渡、体验割裂);③ 破坏依赖反射的框架(Spring/Hibernate/Jackson 靠反射深度访问,模块化后没 opens 就 InaccessibleObjectException,要加 opens/--add-opens)。现实:JDK 自身彻底模块化,但大多数业务应用不写 module-info、还用 classpath(享受不到强封装但避开迁移麻烦)。面试知道「是什么、解决什么、为什么普及慢」即可。理解「JPMS 普及慢原因:迁移成本高/生态未跟上(自动模块过渡)/破坏依赖反射的框架(没 opens 就异常);现实 JDK 模块化了但业务应用还用 classpath」,就理解了 JPMS 的现实处境。
记忆钩子:「JPMS(Java 9 模块系统,代号 Jigsaw)=在包之上加模块层,用 module-info.java 声明依赖和导出;解决三问题:①强封装(exports 导出的包才能被外部访问、没导出的是模块私有真封装,opens 开放反射深度访问)②显式依赖(requires 声明依赖、编译/启动时检查齐全早失败=可靠配置、requires transitive 传递依赖、java.base 隐式依赖)③拆分庞大 JDK(拆成几十模块 java.base/java.sql、jlink 裁剪精简运行时适合容器);★叫好不叫座:迁移成本高/生态未跟上(自动模块)/破坏依赖反射框架(没 opens 就 InaccessibleObjectException),JDK 模块化了但业务应用还用 classpath;面试知道是什么解决什么为什么普及慢即可」。
七、常见误区与追问
- 误区:模块就是包的另一种叫法。 不是——包是命名空间 + 包级访问控制;模块是包之上的更大封装单元 + 显式依赖声明(module-info.java),一个模块含多个包,且只有 exports 的包才能被外部访问(强封装),requires 声明模块依赖。
- 误区:public 类在模块化后还是全世界可见。 不是——模块化后,一个 public 类只有它所在的包被 exports 导出,才能被别的模块访问;没导出的包里的 public 类是「模块私有」的、外部访问不了;这实现了真正的封装(public 不再等于全局可见)。
- 误区:exports 一个包,反射就能深度访问它。 exports 只允许编译期/运行期访问 public 成员;反射的深度访问(访问 private、setAccessible)需要 opens;没 opens 时反射深度访问会抛 InaccessibleObjectException;序列化框架(Jackson)、DI 框架(Spring)常需要 opens。
- 误区:所有 Java 项目 Java 9 后都用了模块系统。 大多数业务应用没用——不写 module-info、还是用 classpath;JPMS 迁移成本高、生态未完全跟上、且破坏了很多依赖反射的框架,所以应用层普及率很低;只有 JDK 自身和部分库彻底模块化了。
- 追问:JPMS 的「强封装」是怎么实现的? 通过 exports——模块里只有被 exports 导出的包,其 public 类型才能被别的模块访问;没导出的包(内部实现)外部完全访问不了(连 public 类都不行、反射也默认不行);这样模块作者能明确区分「API(导出)」和「内部实现(不导出)」,内部实现可以自由修改而不怕被外部依赖。
- 追问:requires 和 requires transitive 有什么区别? requires M:声明本模块依赖 M(只有本模块能访问 M);requires transitive M:依赖 M 且传递——依赖本模块的其他模块也自动能访问 M;用于「本模块的 API 在方法签名里暴露了 M 的类型」的场景(这样使用者不用再手动 requires M)。
- 追问:jlink 有什么用?和模块化什么关系? jlink 是模块化带来的能力——它能根据你的应用需要的模块,生成一个「只包含这些模块 + 依赖」的自定义精简运行时(自定义 JRE);因为 JDK 模块化了、依赖关系明确,jlink 才能精确裁剪;生成的运行时体积小很多(几十 MB vs 几百 MB 的完整 JDK),适合容器化部署(镜像更小)、嵌入式设备。
八、加强记忆
Java 9 的模块系统(JPMS,代号 Jigsaw)在「包」之上加了一层「模块」——用 module-info.java 显式声明「依赖哪些模块(requires)、导出哪些包(exports)」。解决三个核心问题:① 强封装——过去 public 类全世界可见,模块化后只有 exports 的包才能被外部访问、没导出的包是模块私有(真正封装,内部实现可自由改);opens 开放包给反射深度访问(exports 只允许 public 访问,反射访问 private 需 opens,否则 InaccessibleObjectException,序列化/DI 框架常需要);② 显式依赖——requires 声明依赖,编译/启动时检查模块齐全(早失败、可靠配置,避免运行时 ClassNotFoundException),requires transitive 传递依赖,java.base 隐式依赖;③ 拆分庞大 JDK——JDK 拆成几十个模块(java.base/java.sql),配合 jlink 裁剪出只含用到模块的精简运行时(体积小、适合容器)。现实:JPMS 概念先进但「叫好不叫座」——迁移成本高、生态未跟上(自动模块过渡)、破坏了依赖反射的框架;JDK 自身彻底模块化了,但大多数业务应用还用 classpath(不写 module-info)。面试知道「是什么、解决什么、为什么普及慢」即可。一句话「JPMS(Java9 模块系统)=包之上加模块层用 module-info 声明 requires/exports;解决强封装(只有 exports 的包外部可见、opens 给反射)、显式依赖(requires 编译/启动检查早失败)、拆分 JDK(几十模块+jlink 裁剪精简运行时);叫好不叫座:迁移成本高/破坏依赖反射框架,业务应用还用 classpath」。