第 2 章:依赖配置声明:implementation vs api vs compileOnly vs runtimeOnly

正确声明依赖配置,是控制传递链、加速增量编译和减少构建不稳定性的核心手段。一旦用错配置,轻则泄露无关依赖到消费端,重则运行时直接崩溃。本章逐一拆解 Java Library 插件下四种常用配置的语义、传递规则和选型决策。

四种配置的核心差异

配置编译期可见范围运行时可见范围是否可传递典型场景
implementation仅当前模块传递否内部实现细节,不对外暴露
api传递到消费者传递是公共接口类型依赖(如 Spring Boot Starter)
compileOnly仅当前模块编译期无否编译期注解处理器(Lombok)、Servlet API
runtimeOnly无传递否数据库驱动、日志实现(SLF4J 绑定)
优先使用 implementation 而非 api 可以显著缩小编译类路径范围,Gradle 会因此跳过更多上游变更带来的重新编译,这是构建加速的最直接手段之一。
依赖配置:implementation vs api 可见性对比 依赖配置可见性对比 模块 A(消费者) 模块 B(生产者) 模块 C(传递依赖) api(传递暴露) implementation(不传递) 模块 A 可见 C 模块 A 不可见 C

配置声明示例

在 build.gradle.kts 中应用 Java Library 插件后,即可使用 api 与 implementation 配置。

plugins {
    `java-library`
}

dependencies {
    // 通过 api 暴露:消费者编译时可见
    api("com.fasterxml.jackson.core:jackson-databind:2.15.2")

    // 通过 implementation 隐藏:仅当前模块编译时可见
    implementation("org.springframework.boot:spring-boot-starter-logging:3.1.0")

    // compileOnly:编译时需要,运行时由运行环境提供(如 Tomcat)
    compileOnly("jakarta.servlet:jakarta.servlet-api:6.0.0")

    // runtimeOnly:编译时不需要,仅运行时需要
    runtimeOnly("org.postgresql:postgresql:42.6.0")
}

调试命令:验证你的类路径

使用 dependencies 任务查看特定配置的依赖树,确认传递性是否符合预期。

# 查看 runtimeClasspath 的依赖树
./gradlew :my-app:dependencies --configuration runtimeClasspath

# 查看 compileClasspath 的依赖树
./gradlew :my-app:dependencies --configuration compileClasspath

使用 dependencyInsight 追踪某个传递依赖的来源与配置。

./gradlew :my-app:dependencyInsight --dependency jackson-databind --configuration compileClasspath
小贴士:如果发现某个库被错误地通过 api 传递给了消费者,可以在依赖声明中使用 api("com.example:lib:1.0") { isTransitive = false } 或在消费者端使用 exclude 限制传递范围。

常见错误与优化策略

错误场景后果修正/优化方案
将内部实现依赖声明为 api泄露依赖给消费者,增加消费者编译负担,引发不必要的重新编译改为 implementation
将运行时必备依赖声明为 compileOnly编译通过,但运行时抛出 ClassNotFoundException / NoClassDefFoundError改为 implementation 或 api
将编译期必要依赖声明为 runtimeOnly编译时直接失败改为 implementation 或 compileOnly
过度使用 api 导致构建时间膨胀大型项目中,增量构建速度下降明显严格遵循最少暴露原则,接口/基类用 api,实现细节用 implementation
注意:在 Kotlin Multiplatform 或 Android 多模块项目中,api 和 implementation 的语义与 Java Library 插件保持一致,但需要额外留意 source set 的配置。

性能优化:增量编译与任务缓存

Gradle 的增量编译和构建缓存依靠输入文件的快照来判断任务是否过时。当依赖配置从 api 改为 implementation 后,消费者模块的编译类路径缩小,Gradle 可以更精确地判断变更是否影响消费者,从而跳过不必要的重新编译。

关键指标:

  • 使用 api:消费者编译类路径包含传递依赖。若传递依赖变更,消费者任务必定重新执行。
  • 使用 implementation:消费者编译类路径不包含传递依赖。传递依赖变更仅触发直接依赖模块的重新编译,消费者任务可安全地从缓存加载。
在大型项目中,将不必要的 api 替换为 implementation,通常可以减少 30%~50% 的不必要重新编译。