模块系统解决什么

Java 应用长期依赖 classpath 加载类,所有 jar 都在同一个大口袋里,谁依赖谁、谁暴露谁完全靠约定。类冲突、版本漂移、找不到类这些问题,往往到运行期才爆发。

JPMS(Java Platform Module System,模块系统)从 Java 9 开始引入,给代码加上了「边界」:每个模块声明自己依赖什么(requires)、向谁开放什么(exports)。违反边界的引用在编译期就会被拦下。

模块化带来两个直接收益:一是依赖关系显式化,架构边界由编译器强制,而不是靠代码评审提醒;二是配合 jlink 可以只打包运行所需的模块,产出一个精简的定制运行时。

模块是 Java 里写得最清楚的一份架构文档,因为它能被执行。
本文核心知识点
  • module-info模块声明文件。
  • requires依赖声明。
  • exports导出控制。
  • jlink精简运行时打包。
Java 模块系统配图:蓝调抽象插画中金属框架展开的立方体周围环绕半透明蓝色立方体模块嵌合的科技场景
一个清晰的边界框架,把模块各归其位。

module-info.java 基础

每个模块的根目录放一个 module-info.java,它就是模块的「身份证」:声明模块名、依赖、导出、打开的服务。模块名建议沿用反向域名惯例,保证全局唯一。

module com.jiqun.cloud.order {
    requires java.sql;
    requires spring.context;
    exports com.jiqun.cloud.order.api;
}

一旦给项目加上 module-info.java,整个项目就进入了模块化模式,classpath 上的所有依赖都必须能找到对应模块。这也是迁移时最花力气的一步。

模块化项目的入口类编译和运行都要加 `--module-path`,不再用 classpath。IDE 和新版 Maven/Gradle 已经很好地支持模块路径,手动命令行的习惯要调整。

requires 与 exports 控制

requires 声明「我需要谁」,exports 声明「我开放什么」。一个包如果没有被 exports,模块外的代码就无法引用它——内部实现细节从此真正私有。

指令作用
requires声明对另一个模块的依赖
requires transitive传递依赖,下游自动获得
exports导出包,对外开放
exports … to限定导出给指定模块
opens打开包供反射访问
module com.jiqun.cloud.user {
    requires transitive com.jiqun.cloud.common;
    exports com.jiqun.cloud.user.api;
    opens com.jiqun.cloud.user.model to com.fasterxml.jackson.databind;
}

requires transitive 在门面模块里很常用:调用方依赖门面模块,就自动拿到它的传递依赖,不必层层声明。opens 是反射(如 JSON 序列化、依赖注入)需要的出口,反射无法穿透未打开的包。

服务加载与 SPI

模块系统还强化了 ServiceLoader 机制:模块可以声明 provides(提供实现)和 uses(消费服务),让「接口与实现解耦」在模块层面显式化。

// 接口模块
module com.jiqun.cloud.spi {
    exports com.jiqun.cloud.spi;
}

// 实现模块
module com.jiqun.cloud.impl {
    requires com.jiqun.cloud.spi;
    provides com.jiqun.cloud.spi.Greeter
        with com.jiqun.cloud.impl.CnGreeter;
}

// 消费方
ServiceLoader.load(Greeter.class)
    .stream()
    .map(ServiceLoader.Provider::get)
    .forEach(g -> g.sayHello());

运行期用 `--add-modules` 把实现模块加进模块图,ServiceLoader 就能找到实现。插件化的系统用这套机制可以做到「加一个 jar 就多一个能力」,不需要改主程序代码。

既有项目模块化迁移

把老项目整体模块化是重活,业界常见的路径是「自底向上」:先给底层、无依赖的模块加 module-info,再逐层向上,直到最顶层的应用模块。每一层模块化后都能独立编译验证。

第三方的 jar 大多数已经是自动模块(Automatic Module),模块名从 Manifest 或文件名推导。遇到没有模块名的老 jar,可以用 `--add-modules` 和 `--add-exports` 临时放行,先把主线跑通。

迁移期间可以使用未命名模块兼容:部分模块化、部分留在 classpath,逐步推进。模块化不是一次性革命,而是一段可以边走边看的旅程。