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:依赖配置可见性流程图

依赖配置类型 – 编译/运行时可见性 模块 A (生产者) 模块 B (中间模块) 模块 C (消费者) 依赖 依赖 编译期:模块 B 可以看到 A 中由 api/implementation/compileOnly 导出的类 运行时:模块 C 只能看到 A 中由 api 显式导出的类 (implementation 隐藏) api:公开 API 依赖,传递到消费方编译期 implementation:内部实现依赖,对消费方隐藏 compileOnly:仅编译,不参与运行 runtimeOnly:仅运行,编译不可见

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 命令,可以快速诊断配置错误,避免连锁编译和运行时事故。