JPMS 的用法可以浓缩成三步:在源码根目录写一个 module-info.java、编译和运行时把 --class-path 换成 --module-path、用 exports/opens 决定哪些包对外可见。JPMS(Java Platform Module System,即 JSR 376 定义的 Java 模块系统)从 Java 9(2017 年 9 月发布)开始随 JDK 提供,核心价值是强封装和可靠配置,它不是必选项,应用层不迁移也能正常跑。
JPMS 和 Maven 的多模块是一回事吗?
结论:完全不是。JPMS 是 JDK 层面的模块系统,编译期和运行期都生效;Maven 的「多 module 工程」只是源码目录的拆分方式,编译产物仍是普通的 classpath jar。
判断标准很简单:只有在你的源码里出现 module-info.java,这个工程才算真正用了 JPMS。只写 pom.xml 的 <modules> 不算。这也是很多团队误以为「我们已经模块化了」的原因。
module-info.java 怎么写?
结论:一个模块最多只能有一个 module-info.java,位置在模块源码的根目录(默认包下),里面只放模块声明,不写业务代码。
module com.example.order {
requires java.sql; // 编译和运行都要这个模块
requires transitive com.example.core; // 依赖我的模块,也能看到 core
requires static com.example.anno; // 只编译期需要,运行时可缺
exports com.example.order.api; // 只有这个包对外可见
opens com.example.order.entity to com.fasterxml.jackson.databind; // 允许反射
uses com.example.spi.PaymentProvider;
provides com.example.spi.PaymentProvider with com.example.order.AlipayProvider;
}
关键指令只有六个:requires 声明依赖,requires transitive 把依赖传递出去,requires static 表示可选依赖;exports 导出包(可加 to 限定给谁);opens 只开反射不导出 API;uses / provides ... with 对应 ServiceLoader 的服务发现。没有 exports 的包,默认在编译和运行期都被强封装,外部直接访问会报错。
怎么编译、打包和运行?
结论:命令层面就是把 --class-path 替换成 --module-path,把 -cp 替换成 -m。
# 编译
javac -d mods/com.example.order --module-path mods \
$(find src/com.example.order -name '*.java')
# 运行
java --module-path mods -m com.example.order/com.example.order.Main
jar --create --file=mods/com.example.order.jar \
--main-class=com.example.order.Main \
-C mods/com.example.order .
-m 后面的格式是 模块名/主类全限定名,斜杠不能少。
依赖的第三方 jar 没有 module-info 怎么办?
结论:放在类路径(classpath)上的所有代码都属于「未命名模块」(unnamed module),它能看到所有模块;反过来,命名模块不能 require 未命名模块。
所以依赖没模块化时只有两条路:要么把它放到 --module-path 上,JDK 会当成「自动模块」(automatic module)处理,模块名优先取 MANIFEST.MF 里的 Automatic-Module-Name,没有就用 jar 文件名推导(去版本号、非字母数字换成点);要么整个应用干脆不写 module-info.java,继续用类路径。自动模块会读取所有模块并导出所有包,代价是名字可能随 jar 文件名变化,所以给库打 Automatic-Module-Name 是稳妥做法。
运行时报 InaccessibleObjectException 怎么办?
结论:这是强封装生效的正常结果,用 --add-opens、--add-exports、--add-modules 临时放开即可。
java --add-opens java.base/java.lang=ALL-UNNAMED \
--add-modules java.sql \
--module-path mods -m com.example.order/com.example.order.Main
ALL-UNNAMED 表示「类路径上的代码」。--add-opens m/p=target 放开反射,--add-exports m/p=target 放开编译期访问。依赖反射和运行时扫描的框架(如 Spring、Hibernate)经常需要这两类参数。
迁移时最容易踩的坑有哪些?
结论:三个坑最致命——循环依赖、拆分包、反射失效。
模块之间不允许循环 requires,编译直接报错,而 Maven 里循环依赖有时能勉强跑起来;「拆分包」指两个模块导出同一个包名,JVM 会报错拒绝启动,这是老 jar 迁移不动的最常见原因;反射方面,只有 opens 的包才允许外部反射访问非 public 成员。
工具可以省力:jdeps --generate-module-info 生成 module-info.java 草稿,jlink --module-path $JAVA_HOME/jmods --add-modules java.base --output myjre 能裁剪出自定义的运行时镜像。
结论:什么场景值得上 JPMS
库和框架作者、需要 jlink 裁剪体积、需要强封装保护内部 API 的项目,上 JPMS 收益明确;普通业务应用继续用类路径,成本更低。JPMS 是可选能力,不是 Java 9 之后的强制要求。