Java 模块

最后更新: 2026-02-19 04:54:04 +0800 +0800
tags: Java | Jpms | Modules
原文以英文撰写。您正在阅读AI翻译版本。

Java 平台模块系统(JPMS)使依赖关系和包封装变得明确。命名模块包含一个描述符, 即源代码根目录中的 module-info.java。编译它会在展开的模块目录或模块化 JAR 中生成 module-info.class。

java
module com.example.app {
    requires com.example.codec;
    uses com.example.codec.Decoder;
}

module com.example.codec {
    exports com.example.codec.api;
    exports com.example.codec.internal to com.example.app;
    opens com.example.codec.model to com.example.json;
    provides com.example.codec.Decoder with com.example.codec.JsonDecoder;
}

编译、打包和启动 #

传统的多模块源代码树会在模块源路径下为每个模块设置一个目录。使用以下命令编译:

sh
javac --module-source-path src -d out -m com.example.app,com.example.codec
jar --create --file app.jar --main-class com.example.app.Main -C out/com.example.app .
java --module-path libs:app.jar --module com.example.app

jar --main-class 会记录可执行主类,但它是可选的,因为 java -m module/name.Main 可以在启动时指定主类。jlink 根据模块的传递闭包创建运行时映像;它在模块路径上接受模块化 JAR 和 JMOD,而 JMOD 不是运行时产物。

sh
jlink --module-path "$JAVA_HOME/jmods:libs" \
      --add-modules com.example.app --output image
image/bin/java -m com.example.app

可读性和封装 #

requires 使一个模块可读。requires transitive 将这种可读性重新导出给依赖者; requires static 在编译时是必需的,但在运行时是可选的。exports 授予对包中公共类型的编译时和普通运行时访问权限,可以授予所有读取模块,也可以通过 to 授予命名列表中的模块。opens 授予深度反射权限,但不授予普通类型访问权限;open module 会打开其全部包。与框架集成时,应优先使用限定的 opens。

uses 和 provides ... with ... 在模块描述符中声明服务。ServiceLoader 在模块解析期间使用这些声明。包不是独立于模块的访问单位,因此仅有 public 永远不能绕过未导出的包。

解析和迁移 #

在每个阶段,模块路径加上系统模块和升级模块构成可观察模块集合。解析从根模块开始,并沿着 requires 边继续;重复的模块名称会导致失败,而不会允许类路径顺序选择其中一个。命名模块不能包含拆分包:同一个包出现在两个已解析的命名模块中会产生错误。这条规则使包所有权和封装具有确定性。

类路径代码属于未命名模块。它可以读取已解析命名模块的导出包,其自身的包既会导出也会打开,但命名模块不能声明对未命名模块的 requires。模块路径上的非模块化 JAR 会成为自动模块:它拥有派生名称或清单名称,可以读取所有已解析模块,并导出和打开所有包。自动模块有助于分阶段迁移,但不能提供有意义的封装;在依赖模块边界之前,应将其替换为显式描述符。

跨越命名模块边界进行反射需要普通可访问性,以及用于深度访问的开放包。--add-exports 和 --add-opens 是迁移或部署覆盖选项,不能替代模块拥有的描述符。

参考资料 #

评论

在 GitHub 上阅读或评论(发表评论需要登录)