模块化系统
学习目标
- 能读写
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 文件名和模块名可以不同,但长期维护应保持可读的一致性。
// 说明:module:声明模块名称。
// 文件:src/app.core/module-info.java
module app.core {
}
// 输出:模块描述符声明名为 app.core 的模块。requires:声明模块依赖
requires 让当前模块可读另一个模块,并在编译时检查依赖;只写了依赖不代表对外转发依赖,需要 requires transitive 才会让下游模块自动可读。
// 说明: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 方式访问;未导出的实现包仍可在本模块内部使用。
// 说明:exports:公开稳定 API 包。
// 文件:src/app.core/module-info.java
module app.core {
exports com.example.api;
}
// 输出:其他模块可编译访问 com.example.api 包。导出的是包级边界,不是单个类的修饰符替代品。
opens:为反射开放实现包
opens 主要服务于序列化、依赖注入等深度反射;它不让其他模块直接 import 该包。
// 说明: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。
模块路径编译与运行
模块路径让运行时按模块描述解析依赖;命令中的模块名和主类名必须与源码声明一致。
// 说明:模块路径编译与运行。
// 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 的签名暴露了依赖模块中的类型时,才考虑传递依赖;滥用会让下游隐式绑定更多模块,削弱封装和迁移自由度。
// 作用:通过 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 掩盖缺失。
// 作用:通过 requires static 编译期可选依赖。
// 结果:编译 app.core 时必须解析 optional.annotations,运行镜像中可以不包含该模块。
// 文件:src/app.core/module-info.java
module app.core {
requires static optional.annotations;
}声明模块服务:连接使用方与提供方
服务接口所在模块不需要依赖每个实现模块;实现模块只需声明提供关系,运行时由 ServiceLoader 发现。完整案例见 ServiceLoader 服务发现。
// 使用方
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 ...。
// 作用:通过 open module 整体开放反射。
// 结果:framework.runtime 等模块可对 app.legacy 的全部包执行深度反射,但这些包不会因此被 exports。
// 文件:src/app.legacy/module-info.java
open module app.legacy {
requires framework.runtime;
}自动模块与未命名模块
自动模块和未命名模块方便迁移,但名称、可读性和封装规则不如显式模块稳定;迁移前应检查第三方 JAR 的真实模块名和 split package 风险。
// 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、字体和本地库需求。
// 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 运行时镜像简单案例
// 文件: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 的区别在于前者服务普通编译调用,后者服务运行时深度反射;它们不是可以互换的替代项。
课后小问
为什么反射框架常需要
opens,而业务调用只需要exports? 答案:exports 允许编译期和普通调用访问公共类型,opens 允许运行时深度反射访问非公开成员。 解析:把实现包 exports 出去会扩大 API 面;优先只对指定框架模块开放 opens。什么时候应该使用
requires transitive? 答案:当前模块的公共 API 签名直接暴露依赖模块类型时。 解析:如果依赖只是实现细节,普通 requires 更能避免把依赖传给所有下游模块。
本节小结
- module-info.java 用声明方式表达依赖、封装和服务边界。
- requires、exports、opens、uses、provides 分别解决不同问题。
- 模块路径能强化可读性和封装,但迁移旧 classpath 需要逐步审计。
- jdeps 和 jlink 是辅助工具,最终仍要用真实运行验证模块化结果。
快速回顾
requires读依赖,exports暴露 API,opens开放深度反射。requires transitive会把依赖可读性传给下游。- 服务接口用
uses,实现模块用provides ... with。 - 自动模块和未命名模块是迁移阶段的兼容边界。
阿源的知识库