构建可伸缩模块化变量依赖框架的核心是用requires transitive精准暴露契约而非堆砌依赖,关键在于分层设计、识别依赖意图、明确职责边界;基础模块用requires,门面/sdk模块若方法签名含第三方类型则必须requires transitive,组合服务模块对被聚合sdk用requires transitive、对内部工具类用requires;module-info.java中transitive仅控制模块可读性,不影响包可见性;禁止在仅初始化、测试辅助或spi动态加载场景误用transitive;通过jdeps和jlink验证依赖正确性,并可抽取observability.starter等统一starter模块解耦技术选型。
要构建可伸缩的模块化变量依赖框架,核心不是堆砌 transitive,而是用它精准暴露“契约”,让下游模块只关心接口、不操心底层实现细节。关键在分层设计和依赖意图识别。
明确模块职责边界,分清“用”和“传”
一个模块是否该用 requires transitive,取决于它的角色:
- 基础能力模块(如日志、JSON、配置解析):通常不直接被业务调用,而是被中间层封装——这里用普通 requires 即可
-
门面/SDK 模块(如
com.example.api):对外提供统一接口,且方法签名中含第三方类型(如ResponseEntity<jsonnode></jsonnode>)——必须用 requires transitive -
组合服务模块(如
com.example.order-service):聚合多个 SDK,自身也导出新 API ——对被组合的 SDK 用 requires transitive,对自己内部工具类用普通 requires
实战写法:从 module-info.java 开始建模
以一个订单门面模块为例,其 module-info.java 应这样组织:
module com.example.order.facade {
requires java.base;
requires transitive com.fasterxml.jackson.databind; // 返回值含 JsonNode,下游需理解
requires transitive org.slf4j; // 日志接口被暴露在 public 方法参数中
requires transitive com.example.idgen.api; // ID 生成器类型出现在 facade 方法签名里
exports com.example.order.facade.model;
exports com.example.order.facade.service;
}
注意:没导出的包(如 internal)即使用了 transitive 依赖,也不会影响下游可见性;transitive 控制的是“模块可读性”,不是“包可见性”。
避免污染:禁止 transitive 的三类典型场景
以下情况一旦误加 transitive,就会把实现细节泄露出去,破坏封装:
- 仅在
@PostConstruct或静态块中初始化某客户端(如 RedisTemplate),未出现在任何 public 方法中 - 测试辅助类(如
TestUtils)引入了 JUnit 或 Mockito ——这些应放在test源集,不进 module-info - 通过
ServiceLoader.load()动态加载 SPI 实现,此时依赖是运行时绑定,编译期不应强制传递
验证与演进:用 jdeps 和 jlink 做依赖审计
构建后执行以下命令检查是否意外暴露了不该传的依赖:
jdeps --module-path mods/ --require com.example.order.facade mods/com.example.order.facade.jar
再用 jlink 构建最小运行镜像,如果失败并提示 “module X not found”,说明某个 transitive 依赖缺失或路径错误——这反而是发现漏配的好时机。
随着系统增长,可将高频组合(如 logging + tracing + metrics)抽成统一的 com.example.observability.starter 模块,并用 transitive 对接各厂商实现,下游只需依赖 starter,彻底解耦具体技术选型。










