4. 依赖管理基础:implementation、api、compileOnly、runtimeOnly 区别与选择
4.1 为什么需要理解依赖配置类型
在 Gradle 多模块构建中,依赖声明方式直接影响编译可见性、传递依赖范围、构建缓存命中率和编译速度。错误配置会导致不必要的重新编译、模块泄漏、运行时类缺失或死循环依赖。四种核心配置 implementation、api、compileOnly、runtimeOnly 分别控制依赖在“编译期对依赖模块的可见性”和“运行时是否打包”。
4.2 核心配置对比矩阵
| 配置 | 编译期对当前模块可见 | 运行时对当前模块可用 | 是否传递到消费者编译期 | 典型场景 |
|---|---|---|---|---|
implementation | 是 | 是 | 否 | 内部实现依赖,对外隐藏 |
api | 是 | 是 | 是 | 公开 API 所需的依赖 |
compileOnly | 是 | 否 | 否 | 编译时仅需注解、接口;运行时由容器/框架提供 |
runtimeOnly | 否 | 是 | 否(仅传递运行时) | JDBC 驱动、日志实现等运行时绑定 |
4.3 配置选择的决策流程
关键原则:如果消费者(使用该模块的模块)不需要在编译代码中直接引用依赖中的类,优先用 implementation,它减少重新编译传播;反之用 api。仅编译时需要的用 compileOnly;仅运行时需要的用 runtimeOnly。
4.4 内联 SVG:依赖配置可见性流程图
4.5 实战配置示例与效果解析
4.5.1 在模块 B 的 build.gradle.kts 中声明依赖
// 模块 B 的 build.gradle.kts
dependencies {
// A 中的类仅在 B 内部使用,不暴露给 C
implementation(project(":module-a"))
// 如果 B 将 A 中的某些类暴露在公共接口或基类中,必须用 api
// api(project(":module-a"))
// 仅编译依赖,运行时由容器提供(如 Spring Boot 的内置 Tomcat)
compileOnly("jakarta.servlet:jakarta.servlet-api:6.0.0")
// 仅运行时绑定(如数据库驱动)
runtimeOnly("mysql:mysql-connector-java:8.0.33")
}
4.5.2 观察编译和运行效果
在模块 C 中(依赖模块 B),如果 B 使用 implementation 引入 A,则 C 无法直接引用 A 中的类。如果 B 使用 api,则 C 可以直接 import A 中的类。
// 模块 C 的代码
// 当 B 用 implementation 时,编译报错
// import com.example.modulea.SomeClass; // 错误:无法解析
// 当 B 用 api 时,编译通过
// import com.example.modulea.SomeClass; // 正常
4.5.3 查看 Gradle 依赖报告
# 在终端运行,查看模块C的编译依赖树
./gradlew :module-c:dependencies --configuration compileClasspath
# 输出示例(implementation 隐藏 A 的依赖)
compileClasspath - Compile classpath for source set 'main'.
\--- project :module-b
\--- project :module-a (n) // n 表示不可见,编译链已移除
4.6 常见错误与调试技巧
- 错误:编译期找不到符号 – 常因消费者依赖了
implementation的模块但期望使用其传递依赖。解决方案:将依赖改为api,或消费者直接声明所需依赖。 - 错误:运行时 ClassNotFoundException – 将
compileOnly用于运行时必需的库(如 JDBC 驱动)。解决方案:改为implementation或runtimeOnly。 - 错误:依赖传递导致的多版本冲突 – 使用
api时可能将多个版本拉入消费者。解决方案:使用implementation限制传播,或在消费者中显式排除。 - 调试命令:
./gradlew :<module>:dependencyInsight --dependency <group:artifact>定位依赖来源。
4.7 性能与稳定性优化建议
| 优化方向 | 做法 | 效果 |
|---|---|---|
| 减小编译传播 | 优先用 implementation 而非 api | 减少下游模块的重新编译次数,提升增量构建速度 |
| 避免运行时冲突 | 对容器已有 jar 用 compileOnly,避免打包重复 | 减少包体积,避免运行时 LinkageError |
| 明确运行时依赖 | 对驱动、日志实现用 runtimeOnly | 编译类路径更干净,避免编译错误 |
| 利用 dependency locking | 为依赖添加版本锁定,结合 implementation 使用 | 保证多模块构建可重现 |
4.8 小结
选择正确的依赖配置类型是多模块构建优化的基石。记住:隐藏实现用 implementation,公开 API 用 api,编译工具用 compileOnly,运行时绑定用 runtimeOnly。每次添加依赖前,先思考该依赖是否真的需要暴露给下游编译期,据此选择最窄的可见范围。配合依赖报告和 insight 命令,可以快速诊断配置错误,避免连锁编译和运行时事故。