⭐ 推荐:社区规则条款 V1.0

Java 模块化系统 JPMS 怎么用?

域名注册
域名注册 版主萌新站长 👑黑卡会员
发布于 2026-10-15 20:49 ·5 浏览 ·2 回复
内容摘要

JPMS 用法为在源码根目录写唯一的 module-info.java,用 requires/exports/opens/uses/provides 声明依赖与可见性,编译运行改用 module-path;它不同于 Maven 多模块,只有

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 之后的强制要求。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2131.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
juming
5.0/ 51 人评分
5 星100%
4 星0%
3 星0%
2 星0%
1 星0%

全部回复 2

juming
juming 初级会员萌新站长 1楼 2026-10-15 21:13

结论:JPMS 的落地顺序应该是「先拿一个模块写 module-info.java 跑通编译和运行,再考虑拆第二个」,一上手就全工程模块化,基本都会卡在反射和分包上。

补几个帖子里没展开的实操点。一是 module-info.java 的位置,Maven 工程必须放在 src/main/java 根目录(默认包)下,放到 src/main/java/com/example 里 javac 会直接报找不到模块声明;同时 maven-compiler-plugin 版本要够新,否则编译时不会走模块路径。二是 exports 和 opens 别混:exports 只管编译期可见性和 API 导出,反射访问还要靠 opens。Spring、Hibernate、Jackson 这类框架重度依赖反射,只写 exports 运行时就会抛 InaccessibleObjectException,临时可以用 --add-opens 救急,正式方案还是 opens xxx to 指定模块。

迁移路径上,第三方库没模块化时会被当成 automatic module,名字由 jar 名推导、并不稳定,可以先用 jdeps 分析依赖再动;应用层完全可以先不迁移,留在 unnamed module,它默认能读所有具名模块,兼容性没问题。

最常见的坑是分包(split package):同一个包名出现在两个模块里,JPMS 会直接拒绝加载,老项目里非常普遍,拆模块前先 grep 一遍包名冲突,否则会在启动那一刻才炸出来。

萝卜特棒
萝卜特棒 正式会员萌新站长 #1949 2楼 2026-10-15 21:19
juming:结论:JPMS 的落地顺序应该是「先拿一个模块写 `module-info.java` 跑通编译和运行,再考虑拆第二个」,一上手就全工程模块化,基本都会卡在反射…

认同,尤其 split package 那条,拆模块前先 grep 一遍包名,成本远低于改成运行时才发现。补几个同样高频但帖子里没展开的点。

automatic module 名字不稳定其实有正经解法:给没模块化的第三方 jar 在 MANIFEST.MF 里加一行 Automatic-Module-Name: xxx,名字就固定了,拿不到源码也可以用 maven-jar-plugin 或 shade 重打一遍。迁移前用 jar --describe-module --file a.jar 逐个确认实际推导出来的名字,比运行时抛 NoClassDefFoundError 再回头找划算得多。

另一个容易忽略的规则是:具名模块不能 requires 未命名模块。也就是说不能出现「我的代码模块化了,但依赖的老 jar 还留在 classpath」这种半吊子状态——要么把这些 jar 补上 Automatic-Module-Name 一起放进 module-path,要么整个应用先留在 classpath 别上。这直接决定了迁移是从外层包做起还是整体一起上,建议动手前先定好策略。

还有两点实操细节。--add-opens 是运行期参数,Maven 工程要在 surefire 的 argLine 和启动脚本里各配一份,只配一边就会出现测试全绿、启动即挂。maven-compiler-plugin 建议 3.11.0 以上,因为 3.7.0 起才会识别 module-info.java 并把依赖走模块路径。

最后一个建议:CI 里加一步 java --module-path mods --validate-modules,能在构建阶段就把模块冲突和缺失依赖打出来,不用等到线上启动那一刻。