正确做法是让中间模块用requires transitive透出api中暴露的类型依赖,如core模块导出jsonnode或logger时需声明transitive,service和app只需直接依赖core即可使用;非api使用的内部依赖则用普通requires。

核心思路是:让中间模块把下游真正需要的类型依赖“主动透出”,而不是让最上层模块一层层手动补全所有间接依赖。
识别哪些依赖必须声明为 transitive
关键判断标准是——你的模块是否在 导出的 API 中直接暴露了另一个模块的类型。比如方法返回值、参数、异常类型、泛型边界等,都属于“暴露”。只要出现,就必须用 requires transitive。
- 接口方法返回
JsonNode(来自 Jackson)→ 必须requires transitive com.fasterxml.jackson.core - 公共类构造器接收
Logger(来自java.logging)→ 必须requires transitive java.logging - 仅在私有工具方法里调用
logger.info(),且不对外暴露 Logger 类型 → 用普通requires即可
典型三层嵌套结构中的配置对比
假设结构为:app → service → core,而 core 依赖 logging 和 json,且其导出的接口中使用了这两个模块的类型:
-
错误写法(冗余配置):
—core/module-info.java:仅写requires java.logging; requires com.fasterxml.jackson.core;
—service/module-info.java:被迫加requires java.logging; requires com.fasterxml.jackson.core;
—app/module-info.java:还得再加一遍 -
正确写法(一次透出):
—core/module-info.java:写requires transitive java.logging;和requires transitive com.fasterxml.jackson.core;
—service/module-info.java:只需requires core;,无需重复声明
—app/module-info.java:同样只写requires service;,即可直接 new JsonNode、调用 Logger 方法
避免透出内部实现依赖
transitive 不是“越多越好”。它只服务于 API 可见性,不是为了简化构建配置而滥用。
- 测试依赖(如 JUnit、Mockito)绝不能用 transitive,否则污染所有下游模块
- 仅用于日志记录、指标上报、内部缓存等场景的工具模块,若不导出相关类型,就保持普通
requires - 通过
ServiceLoader加载的 SPI 实现类,通常也不需要 transitive —— 调用方只依赖接口,实现由运行时注入
验证传递是否生效的实操方式
编译是最直接的检验手段。在 app 模块中尝试以下代码:
- 写一个方法,参数类型是
com.fasterxml.jackson.databind.JsonNode - 新建一个对象,类型是
java.util.logging.Logger - 如果编译通过,说明 transitive 生效;若报错 “module not readable” 或 “class not found”,说明传递链断了











