Java模块化系统(JPMS)
约 1849 字大约 6 分钟
javajpmsmodules
2025-03-17
概述
Java平台模块系统(Java Platform Module System,JPMS)是Java 9引入的重大特性,通过 module-info.java 文件定义模块的依赖和导出关系,实现强封装和可靠配置。JPMS使得JDK本身也被模块化(如 java.base, java.sql 等),开发者可以用 jlink 工具创建定制化的运行时镜像。
模块化之前的问题
- JAR Hell:classpath上多个JAR包含同名类,加载顺序不确定
- 弱封装:
public修饰的类对所有代码可见,无法做到"包内public但模块外不可见" - 运行时异常:缺少依赖只在运行时才报
ClassNotFoundException - JRE体积:即使最简单的应用也需要完整的JRE(>200MB)
模块基础
module-info.java
// src/com.example.app/module-info.java
module com.example.app {
requires java.sql; // 依赖java.sql模块
requires transitive com.example.core; // 传递性依赖
requires static com.example.annotation; // 编译时依赖(可选)
exports com.example.app.api; // 导出API包
exports com.example.app.spi to // 仅对特定模块导出
com.example.plugin;
opens com.example.app.model to // 对反射开放(框架需要)
com.fasterxml.jackson.databind;
uses com.example.app.spi.Plugin; // 声明使用的服务接口
provides com.example.app.spi.Plugin // 提供服务实现
with com.example.app.impl.DefaultPlugin;
}关键词说明
| 关键词 | 作用 | 示例 |
|---|---|---|
requires | 声明对另一个模块的依赖 | requires java.sql; |
requires transitive | 传递性依赖(依赖者自动获得) | requires transitive java.logging; |
requires static | 编译时依赖,运行时可选 | requires static lombok; |
exports | 导出包中的public类型 | exports com.example.api; |
exports ... to | 限定导出给特定模块 | exports impl to framework; |
opens | 对反射开放(运行时) | opens model to jackson; |
open module | 整个模块对反射开放 | open module myapp { ... } |
uses | 声明消费某个服务接口 | uses java.sql.Driver; |
provides ... with | 提供服务接口的实现 | provides Plugin with Impl; |
强封装机制
未 exports 的包中的类,即使是 public 的,其他模块也无法访问(编译错误),反射也无法访问(除非 opens)。这是比传统 public/private 更强的封装层。
// 编译错误示例
// 在com.example.app中尝试使用未导出的内部类
import com.example.core.impl.InternalHelper; // 编译错误!
// error: package com.example.core.impl is not visible服务提供者框架(ServiceLoader)
JPMS 增强了传统的 ServiceLoader 机制,用 uses / provides 替代 META-INF/services 文件。
// 服务接口模块
module com.example.spi {
exports com.example.spi;
}
// --- com/example/spi/MessageService.java ---
public interface MessageService {
String getMessage();
}
// 服务消费者模块
module com.example.app {
requires com.example.spi;
uses com.example.spi.MessageService;
}
// 消费服务
ServiceLoader<MessageService> loader = ServiceLoader.load(MessageService.class);
for (MessageService service : loader) {
System.out.println(service.getMessage());
}
// 服务提供者模块
module com.example.provider {
requires com.example.spi;
provides com.example.spi.MessageService
with com.example.provider.EmailMessageService;
}jlink — 自定义运行时
jlink 根据模块依赖图生成最小化的JRE镜像,只包含应用实际需要的模块。
# 创建自定义运行时镜像
jlink \
--module-path target/classes:$JAVA_HOME/jmods \
--add-modules com.example.app \
--output custom-runtime \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages
# 使用自定义运行时启动应用
./custom-runtime/bin/java -m com.example.app/com.example.app.Main典型的JRE体积对比:
| 配置 | 体积 |
|---|---|
| 完整JDK 17 | ~300MB |
| jlink仅java.base | ~30MB |
| jlink + java.sql + java.logging | ~40MB |
| 典型微服务应用 | 4060MB |
JDK模块结构
所有模块隐式 requires java.base,无需显式声明。
迁移策略
无名模块(Unnamed Module)
传统classpath上的JAR自动属于"无名模块",可以访问所有已导出的包。
# 传统方式仍然可以运行(兼容)
java -cp lib/*.jar com.example.Main
# 无名模块可以读取所有命名模块
# 但命名模块不能requires无名模块自动模块(Automatic Module)
将JAR放到 module-path 上(而非classpath),它自动成为"自动模块":
- 模块名 = JAR文件名(去掉版本号,连字符转点号)
- 或者JAR的MANIFEST.MF中指定
Automatic-Module-Name - 自动导出所有包
- 自动 requires 所有其他模块
# 放到module-path上
java --module-path lib/ -m com.example.app
# JAR的MANIFEST.MF中建议添加
Automatic-Module-Name: com.google.guava迁移步骤
常见兼容性问题
反射受限
// 框架(如Jackson、Hibernate)依赖反射访问private字段
// 解决方案1:opens指定包
module myapp {
opens com.example.model to com.fasterxml.jackson.databind;
}
// 解决方案2:open整个模块
open module myapp {
requires com.fasterxml.jackson.databind;
}
// 解决方案3:命令行参数
// java --add-opens com.example.app/com.example.model=com.fasterxml.jackson.databind常用命令行参数
# 添加模块读取关系
--add-reads module1=module2
# 添加导出
--add-exports source.module/package=target.module
# 添加反射开放
--add-opens source.module/package=target.module
# 添加额外模块(不在依赖图中的)
--add-modules java.sql,java.xml.bind
# 示例:Spring Boot常用
java --add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.lang.reflect=ALL-UNNAMED \
-jar myapp.jar常见FAQ
Q: 每个项目都需要模块化吗?
A: 不是。JPMS是可选的,传统classpath方式(无名模块)仍然完全支持。对于小型项目或所有依赖都在classpath上的项目,无需模块化。对于大型项目、库开发者、或需要定制JRE的场景,模块化有明显收益。
Q: Maven/Gradle如何支持JPMS?
A: Maven 3.x通过 maven-compiler-plugin 自动识别 module-info.java。Gradle 6.4+ 原生支持模块化。在 src/main/java 根目录放置 module-info.java 即可。
Q: Spring Boot支持JPMS吗?
A: Spring Boot可以在classpath模式下运行在模块化JDK上(无名模块)。Spring Framework 6 / Spring Boot 3 对JPMS的支持有所改善,但完整的模块化应用仍需要额外配置(opens等)。
Q: 如何查看模块依赖?
A:
# 查看JDK模块列表
java --list-modules
# 查看模块描述
java --describe-module java.sql
# 查看模块依赖图
jdeps --module-path lib/ -m com.example.app总结
JPMS通过 module-info.java 提供了编译时和运行时的强封装和可靠依赖管理。它解决了传统classpath的JAR Hell、弱封装和JRE臃肿问题。虽然完全迁移到JPMS需要一定的工作量,但它带来的好处 — 更好的封装性、更小的运行时镜像、更早的依赖检测 — 在大型项目和库开发中尤为有价值。
贡献者
更新日志
9f6c2-feat: organize wiki content and refresh site setup于