必须用requires transitive:当SDK API签名中使用了依赖模块的类型(如返回或接收第三方类型JsonNode),以确保下游模块能识别该类型并成功编译;否则用普通requires。

核心原则很明确:当你的SDK对外暴露的API签名里用到了某个依赖模块的类型,就必须用 requires transitive;否则就用普通 requires。它不是为了“方便”,而是为了保证下游模块能正确编译和运行。
API方法返回或接收第三方类型时必须加 transitive
比如你封装了一个配置读取器,它的公开方法返回 com.fasterxml.jackson.databind.JsonNode:
- 下游模块调用该方法后,必须能识别并使用
JsonNode类型,否则编译失败 - module-info.java 中必须写:
requires transitive com.fasterxml.jackson.databind; - 如果只写
requires,下游模块就得自己再声明一遍 Jackson 依赖,极易引发版本不一致或 NoClassDefFoundError
日志、序列化、HTTP 客户端等通用能力要透出给使用者
你的 SDK 如果提供统一的日志门面(如 LoggerFactory.getLogger(...)),且返回的是 org.slf4j.Logger,那 SLF4J 就属于公共契约的一部分:
- 用户代码会直接操作这个接口,所以 SLF4J 必须对使用者可见
- 对应声明:
requires transitive org.slf4j; - 同理,若 SDK 内部用 OkHttp 构建了
HttpClient并作为方法返回值暴露出去,OkHttp 也得加transitive
内部工具类或私有实现依赖,一律不用 transitive
例如 SDK 里有个 InternalRetryHelper 类,它用到了 guava.Retryer,但这个类不对外暴露,所有重试逻辑都封装在私有方法中:
- 下游模块根本接触不到
Retryer类型,也不需要知道它的存在 - 这时只需写:
requires com.google.common; - 加
transitive反而污染依赖图,让使用者被迫引入一个他们用不到的库
分层导出 + 传递依赖,形成清晰的 API 边界
一个典型的变量 SDK 可拆为三层:core(基础能力)、adapter(适配不同后端)、api(对外接口):
-
com.example.var.core依赖 Jackson 和加密库,并在内部使用 —— 全部用requires -
com.example.var.adapter.nacos依赖 core 和 Nacos SDK,且部分方法返回io.nacos.api.config.ConfigService—— 对 Nacos 使用requires transitive -
com.example.var.api导出VariableClient接口,其方法参数含com.fasterxml.jackson.databind.JsonNode—— 对 Jackson 使用requires transitive,同时requires transitive com.example.var.adapter.nacos(因为它组合了适配器能力)











