module-info 模块速查
学习目标
- 能独立读懂和编写常见
module-info.java声明。 - 能选择普通依赖、传递依赖、编译期可选依赖和反射开放方式。
- 能把 ServiceLoader 的 uses/provides 与模块路径运行命令连接起来。
核心知识点
专业术语
- module-info.java:模块描述符的源文件,编译后成为模块元数据。
- requires:声明当前模块读取另一个模块。
- exports/opens:分别公开普通 API 和开放深度反射。
- uses/provides:声明服务使用方与实现方,供
ServiceLoader发现。
白话解释与边界
模块描述符解决三类问题:谁能被读取、哪些包对外公开、哪些运行时服务可以被发现。相比 classpath,模块系统会显式检查可读性和封装边界;exports 不等于 opens,requires 也不等于把依赖的包导出给下游。模块系统要求命名和路径一致可解析,迁移 classpath 时要逐步处理自动模块、未命名模块和 split package。
常用用法
module:声明模块
模块名是依赖和运行命令使用的标识;建议使用稳定、唯一且可读的反向域名形式。
// 说明:module:声明模块。
// 文件:src/com.example.app/module-info.java
module com.example.app {
}
// 输出:声明名为 com.example.app 的模块。requires:读取另一个模块
requires 只声明可读性和编译依赖,不会把 com.example.core 的包自动暴露给 com.example.app 的下游。
// 说明:requires:读取另一个模块。
// 文件:src/com.example.app/module-info.java
module com.example.app {
requires java.net.http;
requires com.example.core;
}
// 输出:com.example.app 可读取两个依赖模块。exports:导出公共 API 包
只有导出的包才能被其他模块正常 import;实现包可留在模块内部,避免把内部类和可变细节变成公共契约。
// 说明:exports:导出公共 API 包。
// 文件:src/com.example.core/module-info.java
module com.example.core {
exports com.example.core.api;
}
// 输出:其他模块可编译访问 com.example.core.api 包。exports ... to:限定导出对象
限定导出适合只给指定模块的 SPI 或兼容层;新增授权模块时要同步检查依赖图,避免出现隐含耦合。
// 说明:exports ... to:限定导出对象。
// 文件:src/com.example.core/module-info.java
module com.example.core {
exports com.example.core.spi to com.example.plugin;
}
// 输出:只有 com.example.plugin 获得该包的编译访问权。opens:开放深度反射
开放包允许运行时反射访问非公开成员,但不允许其他模块直接编译调用其中的公共类型;开放范围应尽量小。
// 说明:opens:开放深度反射。
// 文件:src/com.example.model/module-info.java
module com.example.model {
opens com.example.model.entity;
}
// 输出:所有可读取该模块的运行时模块均可深度反射此包。opens ... to:只给指定框架开放
限定 opens 比整体开放更能保留封装,适合序列化或依赖注入框架;框架模块名必须真实存在并可解析。
// 说明:opens ... to:只给指定框架开放。
// 文件:src/com.example.model/module-info.java
module com.example.model {
opens com.example.model.entity to framework.runtime;
}
// 输出:只有 framework.runtime 可深度反射此包。requires transitive:向下游传递可读性
只有公共 API 的签名暴露 java.logging 类型时才考虑传递依赖;实现细节普通 requires 即可。
// 说明:requires transitive:向下游传递可读性。
// 文件:src/com.example.api/module-info.java
module com.example.api {
requires transitive java.logging;
}
// 输出:读取 com.example.api 的下游模块也自动读取 java.logging。不常用但需要知道
requires static:编译时可选依赖
requires static 让编译时可见而运行时可以缺席,适合注解和编译辅助库;实际执行路径使用的库仍必须在运行时提供。
// 作用:通过 requires static 编译时可选依赖。
// 结果:com.example.core 编译时可读取 com.example.annotations,运行时未使用其中类型便可省略该模块。
// 文件:src/com.example.core/module-info.java
module com.example.core {
requires static com.example.annotations;
}uses:声明服务使用方
声明 uses 后,模块中的 ServiceLoader.load(Formatter.class) 才能按模块服务配置发现实现;它不会自动创建任何实现实例。
// 作用:通过 uses 声明服务使用方。
// 结果:com.example.app 可用 ServiceLoader.load(Formatter.class) 查询模块路径中的 Formatter 提供者。
// 文件:src/com.example.app/module-info.java
module com.example.app {
uses com.example.spi.Formatter;
}provides ... with:声明服务提供方
实现类必须实现服务接口,通常需要 public 无参构造器或符合服务提供者约定的 provider 方法;具体组合见 ServiceLoader 服务发现。
// 作用:通过 provides ... with 声明服务提供方。
// 结果:ServiceLoader 查询 Formatter 时可发现并实例化 JsonFormatter。
// 文件:src/com.example.json/module-info.java
module com.example.json {
requires com.example.spi;
provides com.example.spi.Formatter
with com.example.json.JsonFormatter;
}open module:整体开放
open module 允许所有包被深度反射,但仍不等于 exports;它适合迁移阶段,长期最好收敛到具体包和框架模块。
// 作用:通过 open module 整体开放。
// 结果:运行时模块可深度反射 com.example.legacy 的全部包,但这些包仍未作为公共 API 导出。
// 文件:src/com.example.legacy/module-info.java
open module com.example.legacy {
requires framework.runtime;
}构建最小模块:声明依赖并导出 API
编译时使用 javac -d out --module-source-path src -m lib,app,运行时使用 java --module-path out -m app/app.Main;删掉 exports lib.api 就会失去跨模块可见性。
// 文件:src/lib/module-info.java
module lib {
exports lib.api;
}
// 文件:src/lib/lib/api/Message.java
package lib.api;
public class Message {
public static String text() { return "module"; }
}
// 文件:src/app/module-info.java
module app {
requires lib;
}
// 文件:src/app/app/Main.java
package app;
import lib.api.Message;
public class Main {
public static void main(String[] args) {
System.out.println(Message.text());
// 输出:module
}
}迁移到模块路径:隔离模块与传统类路径
模块路径按模块描述解析依赖,classpath 上的旧 JAR 会落入未命名模块;迁移时先检查第三方 JAR 的自动模块名、重复包和反射需求。
// Windows PowerShell 示例
javac -d out --module-source-path src -m app
java --module-path out --module app/app.Main
// 结果:javac 将 app 及其依赖编译到 out,启动 app/app.Main 后标准输出为 modulejdeps:静态依赖审计
jdeps 能发现静态引用和模块关系,但反射、ServiceLoader、配置文件和动态类名可能不在结果中;它不是运行时完整测试。
// 作用:通过 jdeps 静态依赖审计。
// 结果:--check app 打印 app 的模块依赖分析,-s app.jar 输出归档的摘要依赖边。
// Windows PowerShell 示例
jdeps --module-path lib --check app
jdeps --module-path lib -s app.jarjlink:裁剪运行时
jlink 只适用于模块化依赖图,生成结果与操作系统和 CPU 平台相关;打包后仍要验证 TLS、字体、本地库和服务发现。
// 作用:通过 jlink 裁剪运行时。
// 结果:runtime 目录包含 app 及其传递依赖所需的定制 Java 运行镜像。
// Windows PowerShell 示例
jlink --module-path "$env:JAVA_HOME/jmods;out" --add-modules app --output runtime简单案例
// 文件:src/app.api/module-info.java
module app.api {
exports com.example.api;
}
// 文件:src/app.api/com/example/api/Hello.java
package com.example.api;
public class Hello {
public static String message() { return "hello module"; }
}
// 文件:src/app.main/module-info.java
module app.main {
requires app.api;
}
// 文件:src/app.main/com/example/main/Main.java
package com.example.main;
import com.example.api.Hello;
public class Main {
public static void main(String[] args) {
System.out.println(Hello.message());
// 输出:hello module
}
}最小模块项目中,API 模块导出包,应用模块 requires 它;这就是“能读到”和“允许别人读到”的两层关系。
易混点
exports允许普通编译/调用,opens允许深度反射;开放不等于导出。requires是当前模块的依赖,requires transitive才会影响下游模块可读性。uses/provides是服务声明,ServiceLoader 才是运行时发现入口。open module只是反射开放范围更大,不会自动让包成为公共 API。
模块路径与 classpath 的区别是解析和封装规则不同;open module 也只是扩大反射入口,而不是替代 exports 的公共 API 声明。
课后小问
如果模块中有 public 类但没有 exports,其他模块能 import 它吗? 答案:不能,public 只解决 Java 访问修饰符,模块还必须导出所在包。 解析:模块系统和类成员访问是两层边界,缺少 exports 会在编译或运行解析阶段失败。
为什么
requires static不适合真正运行时必需的库? 答案:它允许运行时不提供依赖,实际调用时可能出现缺失模块错误。 解析:只有编译期注解等“不参与运行”的依赖才适合 static,业务执行依赖应使用普通 requires。
本节小结
- module-info.java 描述模块名称、依赖、公开包、反射开放和服务关系。
- requires/exports/opens 是模块封装的三条主线。
- transitive、static、qualified exports/opens 用于更细的依赖控制。
- 模块化命令、jdeps、jlink 要和真实运行、反射及服务发现一起验证。
快速回顾
module命名,requires依赖,exports公开 API。opens开放反射,uses声明使用服务,provides声明实现服务。requires transitive影响下游,requires static允许运行时缺席。- 模块路径强化封装,classpath/自动模块是迁移兼容手段。
阿源的知识库