跳转至

模块系统(JPMS)#

版本信息

这是跨版本专题(不是某个 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)使用
}
  1. :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());
    }
}

模块不走 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.loggingjava.net.httpjava.sql 等,必须显式 requires——这是 JDK 9 起最常见的「找不到类型」原因。
  • split package 禁止:同一个包不能出现在两个模块里;迁移旧库若两个 JAR 含同名包,模块化会直接失败。
  • 循环依赖禁止:A requires B、B requires A 不被允许;架构上要拆解。
  • 反射访问要 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 裁剪、要强封装的库)应认真对待模块化。

参考#