模块系统(JPMS)#
版本信息
- 最低 JDK:9(Final,11/17/21/25 LTS 承接)
- 关键 JEP:261: Module System(核心:模块图、module-path、jlink)|282: jlink(链接器)|260: Encapsulate Internal APIs(封装内部 API)|200: Modular JDK
这是跨版本专题(不是某个 LTS 独有):JDK 9 引入、之后所有版本都基于它。本教程把它单独成篇,因为它从根上改变了「Java 工程如何组织、如何打包、如何控制可见性」。
为什么需要#
JDK 8 的工程全靠 classpath:它是一条扁平、顺序敏感、对内部完全开放的查找路径——
- JAR 地狱:同一类的不同版本都在路径上,先找到的胜出,冲突时只在运行期莫名报错。
- 强封装做不到:
public就是「全世界可见」,没法说「这个类只给我的库内部用」。sun.misc.Unsafe等内部 API 被大量外部依赖直接用,绑死了 JDK 实现。 - 运行时臃肿:哪怕你只用几个类,也得带上整个
rt.jar(JDK 8)的几千个类,无法裁剪。 - 可靠配置缺失:缺了依赖的 JAR,往往要等到运行时调用处才
ClassNotFoundException。
JPMS(Java Platform Module System)用显式模块(module-info.java)解决这些:声明 requires(依赖谁)、exports(暴露哪个包)、opens(允许反射访问哪个包),让依赖与可见性在编译期与启动期被显式校验,并能用 jlink 打出只含所需模块的最小运行时。
核心概念:module-info#
examples/jpms/ 是一个真实可运行的模块化工程(snippets 嵌入,与示例零漂移)。首先是模块描述符:
// 模块描述符(module-info.java):声明本模块的名字、依赖与对外暴露的包。
module com.javamodern.jpms {
requires java.logging; // 显式依赖 java.logging 模块(默认只可见 java.base)
exports com.javamodern.jpms; // 对外暴露此包,供其它模块(或 classpath)使用
}
- :material-lightbulb:
module声明具名模块;requires显式声明依赖(默认只能看到java.base,要java.logging必须显式 requires);exports决定哪些包对外可见(未导出的包对其它模块不可见,哪怕里面的类是public)。
模块里的代码(JpmsDemo,运行时能查到自己所属模块的名字):
package com.javamodern.jpms;
import java.util.logging.Logger;
// 模块化示例:本类位于名为 com.javamodern.jpms 的「具名模块」中(见 module-info.java)。
// 它 requires java.logging、exports com.javamodern.jpms——模块边界在这里被显式声明与校验。
public class JpmsDemo {
private static final Logger LOG = Logger.getLogger(JpmsDemo.class.getName());
public static String greet(String name) {
LOG.info("greeting " + name); // 用到 module-info 里 requires java.logging
Module module = JpmsDemo.class.getModule(); // 具名模块才有名字
return "Hello, " + name + "(from module " + module.getName() + ")";
}
/** 返回本类所属模块的名字(具名模块才有名字;classpath 上的类为 null)。 */
public static String moduleName() {
return JpmsDemo.class.getModule().getName();
}
public static void main(String[] args) {
System.out.println(greet("Java modular"));
System.out.println("module name = " + moduleName());
}
}
怎么用:模块路径与 jlink#
模块不走 classpath,走 module-path。本仓库已用 Maven + toolchain 把它编译为具名模块;下面是纯 JDK 工作流(JDK 17 实测,路径分隔符 Windows 用 ;、Unix 用 :):
# 1) 以模块路径运行(-p = --module-path,-m = 具名模块/主类)
java --module-path examples/jpms/target/classes \
-m com.javamodern.jpms/com.javamodern.jpms.JpmsDemo
# 2) jlink:把「本模块 + 它 requires 的模块(递归)」与一个最小 JVM 链接成独立运行时镜像
jlink --module-path "$JAVA_HOME/jmods;examples/jpms/target/classes" \
--add-modules com.javamodern.jpms \
--launcher jpms=com.javamodern.jpms/com.javamodern.jpms.JpmsDemo \
--output myapp-image
实测输出(JDK 17):
Hello, Java modular(from module com.javamodern.jpms)
module name = com.javamodern.jpms
jlink 产出的 myapp-image/ 仅约 42 MB(含 JVM + java.base + java.logging + 本模块),远小于一个完整 JDK;里面的 bin/jpms 启动器可脱离任何已安装 JDK 直接运行——这正是容器/云原生场景渴望的「自包含、可裁剪」分发。
与 JDK 8 旧写法对比#
# 一长串 JAR 扔到 classpath,顺序敏感、冲突靠运气
java -cp lib/a.jar:lib/b.jar:lib/c.jar:app.jar com.app.Main
# 想裁剪运行时?做不到——必须带完整 JDK/JRE
# sun.misc.Unsafe 等内部 API 谁都能反射用,绑死实现
# 模块图在启动期校验:缺依赖、循环依赖、split package 直接启动失败(早暴露)
java --module-path mods -m com.app/com.app.Main
# jlink 裁剪出只含所需模块的最小镜像,可独立分发
jlink --module-path "$JAVA_HOME/jmods:mods" --add-modules com.app --output app-image
底层原理#
JVM 启动时,按 module-path 上各模块的 module-info 构建模块图:解析每个模块的 requires、校验「无 split package(同一包不得出现在两个模块)、无循环依赖、依赖都能满足」。校验通过才进入 main,否则启动即失败——把「运行期才爆的 ClassNotFound」提前到启动期。exports/opens 由类加载器在访问时强制:未导出的包对其它模块不可见(强封装)。jlink 复用这套模块图,只链接 --add-modules 闭包内的模块 + 一个裁剪过的 JVM。
graph LR
M["module-info<br/>requires / exports / opens"] --> G["启动期构建模块图<br/>校验依赖/无循环/无 split"]
G --> R["运行:exports 控制可见<br/>opens 控制反射"]
G --> J["jlink:链接模块闭包<br/>+ 最小 JVM → 可裁剪镜像"]
style M fill:#bbdefb
style J fill:#c8e6c9
常见坑 / 最佳实践#
- 默认只可见
java.base:要用java.logging、java.net.http、java.sql等,必须显式requires——这是 JDK 9 起最常见的「找不到类型」原因。 - split package 禁止:同一个包不能出现在两个模块里;迁移旧库若两个 JAR 含同名包,模块化会直接失败。
- 循环依赖禁止:A
requiresB、BrequiresA 不被允许;架构上要拆解。 - 反射访问要
opens:框架(Spring、Jackson 等)用反射访问你的字段,对应包要opens(或open module),否则IllegalAccessException——这是迁移的最大摩擦点。 - 三方 JAR 可作 automatic module:未模块化的 JAR 放 module-path 上会自动成为「自动模块」(名字取自
Automatic-Module-Name或 jar 文件名),可作为过渡。 - 内部 API 已封装:
sun.misc.Unsafe等在 JDK 9+ 被强封装,依赖它的库需迁移(多数已迁)。 - 可渐进迁移:不模块化也能用(classpath + 未命名模块),JPMS 不强制;新工程、库、想用 jlink 裁剪时再上。
小结#
JPMS 是 JDK 9 以来影响最深远的架构能力:用显式 module-info 取代 classpath 黑盒,带来强封装、可靠配置与可裁剪分发(jlink)。它有迁移成本,但对库设计、大型工程与云原生分发是值得的投资。本教程其余示例虽多为非模块化的单文件 demo,但生产级工程(尤其要 jlink 裁剪、要强封装的库)应认真对待模块化。