第 2 章:依赖配置声明:implementation vs api vs compileOnly vs runtimeOnly
正确声明依赖配置,是控制传递链、加速增量编译和减少构建不稳定性的核心手段。一旦用错配置,轻则泄露无关依赖到消费端,重则运行时直接崩溃。本章逐一拆解 Java Library 插件下四种常用配置的语义、传递规则和选型决策。
四种配置的核心差异
| 配置 | 编译期可见范围 | 运行时可见范围 | 是否可传递 | 典型场景 |
|---|---|---|---|---|
| implementation | 仅当前模块 | 传递 | 否 | 内部实现细节,不对外暴露 |
| api | 传递到消费者 | 传递 | 是 | 公共接口类型依赖(如 Spring Boot Starter) |
| compileOnly | 仅当前模块编译期 | 无 | 否 | 编译期注解处理器(Lombok)、Servlet API |
| runtimeOnly | 无 | 传递 | 否 | 数据库驱动、日志实现(SLF4J 绑定) |
优先使用
implementation 而非 api 可以显著缩小编译类路径范围,Gradle 会因此跳过更多上游变更带来的重新编译,这是构建加速的最直接手段之一。配置声明示例
在 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% 的不必要重新编译。