Skip to content

模块化系统 ​

学习目标 ​

  • 能读写 module-info.java 中的依赖、导出、开放和服务声明。
  • 能区分类路径、模块路径、未命名模块和自动模块。
  • 能为反射框架、插件发现和精简运行时选择合适的模块边界。

核心知识点 ​

专业术语 ​

  • JPMS(Java Platform Module System):Java 9 引入的模块系统,用声明文件表达依赖和封装。
  • 可读性(readability):一个模块能否读取另一个模块导出的公共类型。
  • exports:对编译和普通运行时调用公开包中的公共类型。
  • opens:只为深度反射开放运行时访问,不等于导出公共 API。
  • 服务(service):用接口和 uses/provides 解耦实现方与使用方的插件机制。

白话解释与边界 ​

module-info.java 是模块的声明契约:requires 解决“我依赖谁”,exports 解决“别人能调用什么”,opens 解决“框架能否反射访问”。模块化可以减少意外依赖和 split package,但不能自动修复糟糕的包设计。服务发现可继续查阅 module-info 模块速查 和 ServiceLoader 服务发现。旧 classpath 应用可以逐步迁移,不必一次把所有第三方库都模块化。

常用用法 ​

module:声明模块名称 ​

模块名是稳定的依赖标识,应使用反向域名或项目约定;目录名、JAR 文件名和模块名可以不同,但长期维护应保持可读的一致性。

java
// 说明:module:声明模块名称。
// 文件:src/app.core/module-info.java
module app.core {
}
// 输出:模块描述符声明名为 app.core 的模块。

requires:声明模块依赖 ​

requires 让当前模块可读另一个模块,并在编译时检查依赖;只写了依赖不代表对外转发依赖,需要 requires transitive 才会让下游模块自动可读。

java
// 说明:requires:声明模块依赖。
// 文件:src/app.cli/module-info.java
module app.cli {
    requires app.core;
    requires java.net.http;
}
// 输出:app.cli 可读取 app.core 与 java.net.http。

exports:公开稳定 API 包 ​

只有导出的包才能被其他模块按普通 Java 方式访问;未导出的实现包仍可在本模块内部使用。

java
// 说明:exports:公开稳定 API 包。
// 文件:src/app.core/module-info.java
module app.core {
    exports com.example.api;
}
// 输出:其他模块可编译访问 com.example.api 包。

导出的是包级边界,不是单个类的修饰符替代品。

opens:为反射开放实现包 ​

opens 主要服务于序列化、依赖注入等深度反射;它不让其他模块直接 import 该包。

java
// 说明:opens:为反射开放实现包。
// 文件:src/app.core/module-info.java
module app.core {
    exports com.example.api;
    opens com.example.model to framework.runtime;
}
// 输出:framework.runtime 可深度反射 com.example.model 包。

能用公开构造器或接口完成的事情,不要把整个模块改成 open module。

模块路径编译与运行 ​

模块路径让运行时按模块描述解析依赖;命令中的模块名和主类名必须与源码声明一致。

java
// 说明:模块路径编译与运行。
// Windows PowerShell 示例
javac -d out --module-source-path src -m app.core,app.cli
java --module-path out -m app.cli/com.example.cli.Main
// 输出:编译两个模块并运行 app.cli 的主类(前提:源码路径与模块声明一致)。

迁移时可以先用 classpath 验证业务,再逐步切到 --module-path。

不常用但需要知道 ​

requires transitive:传递可读性 ​

只有当公共 API 的签名暴露了依赖模块中的类型时,才考虑传递依赖;滥用会让下游隐式绑定更多模块,削弱封装和迁移自由度。

java
// 作用:通过 requires transitive 传递可读性。
// 结果:依赖 app.api 的模块也可读取 java.logging,无需再次声明 requires java.logging。
// 文件:src/app.api/module-info.java
module app.api {
    requires transitive java.logging;
}

requires static:编译期可选依赖 ​

requires static 表示编译时需要、运行时可以不提供,适合注解和编译辅助库;如果运行时路径真的会调用该依赖,不能靠 static 掩盖缺失。

java
// 作用:通过 requires static 编译期可选依赖。
// 结果:编译 app.core 时必须解析 optional.annotations,运行镜像中可以不包含该模块。
// 文件:src/app.core/module-info.java
module app.core {
    requires static optional.annotations;
}

声明模块服务:连接使用方与提供方 ​

服务接口所在模块不需要依赖每个实现模块;实现模块只需声明提供关系,运行时由 ServiceLoader 发现。完整案例见 ServiceLoader 服务发现。

java
// 使用方
module app.cli {
    uses com.example.spi.Formatter;
}

// 提供方
module app.json {
    provides com.example.spi.Formatter
            with com.example.json.JsonFormatter;
}
// 结果:模块解析后 app.cli 声明 Formatter 服务用途,app.json 注册 JsonFormatter 提供者,ServiceLoader 可发现该实现

open module:整体开放反射 ​

open module 会让所有包对深度反射开放,但仍不等于导出 API;它适合迁移旧框架的过渡阶段,长期应收窄为特定包的 opens ... to ...。

java
// 作用:通过 open module 整体开放反射。
// 结果:framework.runtime 等模块可对 app.legacy 的全部包执行深度反射,但这些包不会因此被 exports。
// 文件:src/app.legacy/module-info.java
open module app.legacy {
    requires framework.runtime;
}

自动模块与未命名模块 ​

自动模块和未命名模块方便迁移,但名称、可读性和封装规则不如显式模块稳定;迁移前应检查第三方 JAR 的真实模块名和 split package 风险。

java
// classpath 上的旧 JAR 不提供 module-info.java:它会进入未命名模块
// module-path 上没有 module-info.java 的 JAR:通常按 Automatic-Module-Name 或文件名生成自动模块名
public class ClasspathMigrationDemo {
    public static void main(String[] args) {
        System.out.println("先验证 classpath,再迁移 module-path");
// 输出:先验证 classpath,再迁移 module-path
    }
}

裁剪运行时:先审计依赖再生成镜像 ​

jdeps 只能帮助发现静态依赖,不能替代运行时 smoke test;jlink 生成的运行时应在目标平台重新验证服务加载、TLS、字体和本地库需求。

java
// Windows PowerShell 示例
jdeps --module-path lib -s app.jar
jlink --module-path "$env:JAVA_HOME/jmods;out" --add-modules app.cli --output runtime
// 结果:jdeps 输出 app.jar 的模块依赖摘要,jlink 生成包含 app.cli 及其依赖的 runtime 运行时镜像

简单案例 ​

java
// 文件:src/app.hello/module-info.java
module app.hello {
    exports com.example.hello;
}

// 文件:src/app.hello/com/example/hello/Hello.java
package com.example.hello;

public class Hello {
    public static String message() {
        return "hello module";
    }
}

// 文件:src/app.main/module-info.java
module app.main {
    requires app.hello;
}

// 文件:src/app.main/com/example/main/Main.java
package com.example.main;

import com.example.hello.Hello;

public class Main {
    public static void main(String[] args) {
        System.out.println(Hello.message());
// 输出:hello module
    }
}

app.main 只能访问 app.hello 导出的包;如果删掉 exports,即使 Hello 是 public,跨模块编译也会失败。

易混点 ​

  • exports 是普通 API 可见性,opens 是深度反射访问;两者不是同义词。
  • requires 让当前模块读取依赖,requires transitive 才会把可读性传递给下游。
  • classpath 的未命名模块与 module-path 的自动模块是迁移工具,不代表设计已经完成。
  • uses/provides 声明服务关系,真正加载实现仍由 ServiceLoader 完成。

exports 与 opens 的区别在于前者服务普通编译调用,后者服务运行时深度反射;它们不是可以互换的替代项。

课后小问 ​

  1. 为什么反射框架常需要 opens,而业务调用只需要 exports? 答案:exports 允许编译期和普通调用访问公共类型,opens 允许运行时深度反射访问非公开成员。 解析:把实现包 exports 出去会扩大 API 面;优先只对指定框架模块开放 opens。

  2. 什么时候应该使用 requires transitive? 答案:当前模块的公共 API 签名直接暴露依赖模块类型时。 解析:如果依赖只是实现细节,普通 requires 更能避免把依赖传给所有下游模块。

本节小结 ​

  • module-info.java 用声明方式表达依赖、封装和服务边界。
  • requires、exports、opens、uses、provides 分别解决不同问题。
  • 模块路径能强化可读性和封装,但迁移旧 classpath 需要逐步审计。
  • jdeps 和 jlink 是辅助工具,最终仍要用真实运行验证模块化结果。

快速回顾 ​

  • requires 读依赖,exports 暴露 API,opens 开放深度反射。
  • requires transitive 会把依赖可读性传给下游。
  • 服务接口用 uses,实现模块用 provides ... with。
  • 自动模块和未命名模块是迁移阶段的兼容边界。