Skip to content

第 6 章 依赖管理:implementation / api / 冲突解决

学习目标:彻底搞懂 implementation / api / compileOnly / runtimeOnly 五兄弟的区别;理解传递依赖(transitive)和依赖冲突的自动仲裁;学会用 ./gradlew dependencies 排查问题;会用 platform 和 enforced platform 锁版本;知道仓库优先级和镜像配置。


6.1 用一个生活化的例子讲清"依赖"

想象你开了一家烧烤店:

   ┌──────────────────────────────────────────────────────────┐
   │  你的烧烤店(your-app)需要:                                │
   │  ① 烤架(log4j)            → 直接依赖                      │
   │  ② 食材供应商(spring-boot)  → 直接依赖                      │
   │                                                              │
   │  食材供应商又依赖:                                            │
   │  ② → 物流公司(jackson)        → 传递依赖                    │
   │  ② → 包装厂(snake-yaml)       → 传递依赖                    │
   │  ② → 物流公司又依赖快递员(asm)  → 传递依赖的传递依赖           │
   │                                                              │
   │  最终:你写一行 implementation("spring-boot")                  │
   │  Gradle 自动拉取一棵深度 3-5 的依赖树(约 30-50 个 jar)      │
   └──────────────────────────────────────────────────────────┘

依赖管理 = 自动化的"供应链管理":你只声明直接需要的,Gradle 帮你拉所有传递依赖、解决冲突、保证版本一致。


6.2 仓库(Repositories):从哪儿拉

6.2.1 三大公共仓库

kotlin
repositories {
    mavenCentral()       // ① 最大的 Java 公共仓库(Maven Central)
    google()             // ② Google 的 Maven 仓库(Android、Jetpack 在这里)
    gradlePluginPortal() // ③ Gradle 插件市场(在 settings.gradle.kts pluginManagement 里)
}

6.2.2 国内镜像加速

kotlin
repositories {
    maven("https://maven.aliyun.com/repository/public")     // 阿里云中央镜像
    maven("https://maven.aliyun.com/repository/google")     // 阿里云 Google 镜像
    maven("https://maven.aliyun.com/repository/gradle-plugin")
    mavenCentral()                                          // 兜底
}

6.2.3 私有仓库 + 凭证

kotlin
repositories {
    maven {
        url = uri("https://nexus.company.com/repository/maven-private")
        credentials {
            username = providers.gradleProperty("nexusUser").get()
            password = providers.gradleProperty("nexusPass").get()
        }
    }
}

nexusUsernexusPass 写到 ~/.gradle/gradle.properties(不进 Git)。

6.2.4 仓库优先级和镜像统一

仓库的查找顺序是声明顺序。但更好的做法是在 settings.gradle.kts 里集中管理

kotlin
// settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS  // 强制集中
    repositories {
        mavenCentral()
        google()
    }
}

6.3 依赖配置(Configurations):implementation 五兄弟

这是面试必考的核心概念。

6.3.1 五种最常用的配置

配置编译可见运行可见传递给消费者典型场景
implementation大多数依赖(默认选)
api库的"公开 API"层依赖
compileOnly只编译需要:Servlet API、Lombok
runtimeOnly只运行需要:JDBC 驱动、Logback 实现
testImplementation仅测试仅测试JUnit 等测试库

6.3.2 详细解释 + 例子

implementation(默认推荐)

kotlin
dependencies {
    implementation("com.google.guava:guava:32.0.0-jre")
}
  • ✅ 你的代码可以 import com.google.common.collect.Lists
  • ✅ 运行时 jar 包在 classpath 里
  • 如果你的项目是个库,依赖你的人看不到 guava —— 他们要用 guava 必须自己声明

api(库的公开依赖)

kotlin
plugins {
    `java-library`   // ← 必须用 java-library 才能用 api
}

dependencies {
    api("com.google.guava:guava:32.0.0-jre")
}
  • 同 implementation,但会传递给消费者
  • 谁依赖你的库,自动也能用 guava

何时用 api vs implementation?

   ┌────────────────────────────────────────────────────────────┐
   │  规则:你的"公共 API"参数 / 返回值类型来自这个依赖时 → api    │
   │       否则 → implementation                                │
   ├────────────────────────────────────────────────────────────┤
   │  例 1:                                                     │
   │  // 我的库导出方法签名用了 Guava 的类                          │
   │  public ImmutableList<String> getNames() { ... }            │
   │  → 必须用 api(guava),否则消费者编译报错"找不到 ImmutableList" │
   │                                                              │
   │  例 2:                                                     │
   │  // 我内部用 Guava 的 Joiner,但导出方法不暴露                │
   │  public String join(List<String> list) {                    │
   │      return Joiner.on(",").join(list);  // ← 内部用          │
   │  }                                                           │
   │  → 用 implementation(guava),消费者无需关心                   │
   └────────────────────────────────────────────────────────────┘

📌 生活化类比:你做了一道菜(库)。api 是"菜里露在外面的食材(番茄)",吃菜的人能看到;implementation 是"调料(盐、糖)",吃菜的人感觉到味道但不知道是啥。

compileOnly(只编译需要)

kotlin
dependencies {
    compileOnly("javax.servlet:javax.servlet-api:4.0.1")  // 编译需要 Servlet API
    compileOnly("org.projectlombok:lombok:1.18.30")       // Lombok 注解
    annotationProcessor("org.projectlombok:lombok:1.18.30")
}
  • 编译期可见
  • 不打进 runtime classpath
  • 不打进 最终 jar
  • 典型场景:
    • Servlet API(servlet 容器会提供)
    • Lombok(代码生成完就不需要了)
    • 编译期类型校验工具(@Nullable 等注解)

runtimeOnly(只运行需要)

kotlin
dependencies {
    runtimeOnly("org.postgresql:postgresql:42.7.0")        // JDBC 驱动
    runtimeOnly("ch.qos.logback:logback-classic:1.4.11")   // 日志实现
}
  • 编译期不可见(你写代码 import 不到)
  • 运行时在 classpath
  • 典型场景:
    • JDBC 驱动(你代码只 import java.sql.*,不直接 import 驱动)
    • 日志实现(slf4j-api 是 implementation,logback 是 runtimeOnly)

6.3.3 完整对照图

                    ┌──────────────────┐
                    │  你的代码 (main)  │
                    └─────────┬────────┘

            ┌─────────────────┼─────────────────┐
            ▼                 ▼                 ▼
    ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
    │implementation│ │     api      │ │  compileOnly │
    │              │ │              │ │              │
    │ ✅ 编译可见   │ │ ✅ 编译可见   │ │ ✅ 编译可见  │
    │ ✅ 运行可见   │ │ ✅ 运行可见   │ │ ❌ 运行不见  │
    │ ❌ 不传递    │ │ ✅ 传递       │ │ ❌ 不打进 jar│
    └──────────────┘ └──────────────┘ └──────────────┘

                        ┌──────────────┐
                        │  runtimeOnly │
                        │              │
                        │ ❌ 编译不见  │
                        │ ✅ 运行可见  │
                        │ ❌ 不传递    │
                        └──────────────┘

6.4 传递依赖与冲突解决

6.4.1 传递依赖是什么

kotlin
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web:3.2.0")
}

实际拉了什么?跑 ./gradlew dependencies

+--- org.springframework.boot:spring-boot-starter-web:3.2.0
     +--- org.springframework.boot:spring-boot-starter:3.2.0
     |    +--- org.springframework.boot:spring-boot:3.2.0
     |    +--- org.springframework.boot:spring-boot-autoconfigure:3.2.0
     |    +--- jakarta.annotation:jakarta.annotation-api:2.1.1
     |    \--- snakeyaml:snakeyaml:2.2
     +--- org.springframework.boot:spring-boot-starter-json:3.2.0
     |    +--- jackson-databind:2.15.3
     |    |    +--- jackson-core:2.15.3
     |    |    \--- jackson-annotations:2.15.3
     |    ...
     ...

总共拉了 ~25 个 jar,全部自动到位。

6.4.2 冲突自动仲裁:默认"最高版本胜出"

kotlin
dependencies {
    implementation("module-a:1.0")     // 拉 guava 30.0
    implementation("module-b:2.0")     // 拉 guava 32.0
}

./gradlew dependencies 输出:

+--- module-a:1.0
|    \--- com.google.guava:guava:30.0 -> 32.0      ← 被升级!
+--- module-b:2.0
     \--- com.google.guava:guava:32.0

-> 表示版本被仲裁后改了。默认策略:选最高版本(更新通常向后兼容)。

6.4.3 强制锁版本

如果你就要用 30.0,不让 Gradle 自动升级:

kotlin
// 方式 1:strict 版本
dependencies {
    implementation("com.google.guava:guava") {
        version {
            strictly("30.0")  // 任何更高版本都报错
        }
    }
}

// 方式 2:全局 force
configurations.all {
    resolutionStrategy.force("com.google.guava:guava:30.0")
}

// 方式 3:用 platform(BOM)
dependencies {
    implementation(platform("com.example:my-bom:1.0"))
    implementation("com.google.guava:guava")  // 版本来自 BOM
}

6.4.4 排除某个传递依赖

kotlin
dependencies {
    implementation("org.springframework:spring-core:6.1.1") {
        exclude(group = "commons-logging", module = "commons-logging")
    }
}

// 全局排除
configurations.all {
    exclude(group = "commons-logging", module = "commons-logging")
}

典型场景:spring-core 默认拉 commons-logging,但你已经用 SLF4J 了 → 排除掉避免冲突。


6.5 platform / BOM:统一一组依赖的版本

6.5.1 痛点:Spring 全家桶版本怎么对齐?

kotlin
// ❌ 自己写版本号,容易错
dependencies {
    implementation("org.springframework:spring-core:6.1.1")
    implementation("org.springframework:spring-web:6.1.0")    // ← 不一致!
    implementation("org.springframework:spring-context:6.1.2") // ← 不一致!
}

6.5.2 解法:用 platform(即 Maven 的 BOM)

kotlin
dependencies {
    implementation(platform("org.springframework:spring-framework-bom:6.1.1"))
    // 下面所有 spring-* 不用写版本,由 platform 决定
    implementation("org.springframework:spring-core")
    implementation("org.springframework:spring-web")
    implementation("org.springframework:spring-context")
}

升级 Spring:只改 platform 的版本号,所有子库自动跟着。

6.5.3 platform vs enforcedPlatform

类型行为
platform"推荐"版本,可以被冲突仲裁覆盖
enforcedPlatform"强制"版本,谁都不能改

⚠️ 少用 enforcedPlatform:会导致依赖冲突时报错而不是自动解决。

6.5.4 Spring Boot 的官方 BOM

Spring Boot 项目的标准做法:

kotlin
plugins {
    id("org.springframework.boot") version "3.2.0"
    id("io.spring.dependency-management") version "1.1.4"  // 隐式管理 BOM
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")    // 不写版本
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

spring-boot-starter-* 的版本由 plugin 注入的 BOM 自动决定,永远一致。


6.6 排查依赖问题的命令

6.6.1 看完整依赖树

bash
$ ./gradlew dependencies                                # 看所有 configuration
$ ./gradlew dependencies --configuration runtimeClasspath  # 看运行时 classpath
$ ./gradlew dependencies --configuration testRuntimeClasspath  # 测试用

6.6.2 看某个依赖是怎么进来的(依赖洞察)

bash
$ ./gradlew dependencyInsight --dependency guava

输出:

> Task :dependencyInsight
com.google.guava:guava:32.0.0-jre (selected by rule)
   variant "compile" [...]

com.google.guava:guava:30.0 -> 32.0.0-jre
+--- module-a:1.0
\--- compileClasspath

com.google.guava:guava:32.0.0-jre
+--- module-b:2.0
\--- compileClasspath

清楚看到 guava 是"被 module-a 间接拉进来 30.0,被 module-b 拉进来 32.0,最终选了 32.0"。

6.6.3 看为什么用了某个版本(按版本号搜)

bash
$ ./gradlew dependencyInsight --dependency guava --configuration runtimeClasspath

6.6.4 看仓库实际拉了哪些

bash
$ ./gradlew --refresh-dependencies build  # 强制重新解析依赖
$ ./gradlew build --info                  # 打印 jar 下载日志

6.7 依赖锁定:可重现构建

如果你要严格锁定依赖版本(比如发布到生产、跑安全审计),用 Dependency Locking

kotlin
dependencyLocking {
    lockAllConfigurations()
}

跑:

bash
$ ./gradlew dependencies --write-locks

会生成 gradle.lockfile

# This is a Gradle generated file for dependency locking.
com.google.guava:guava:32.1.3-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-core:6.1.1=compileClasspath,...
empty=incrementalScalaAnalysisFormain

把这个文件提交到 Git,以后构建时任何依赖版本变化都会报错,强制你显式更新。

💡 类似 npm 的 package-lock.json、Pipenv 的 Pipfile.lock、Cargo 的 Cargo.lock


6.8 Version Catalog 完整示例(结合第 4 章)

toml
# gradle/libs.versions.toml
[versions]
spring-boot = "3.2.0"
junit = "5.10.0"
guava = "32.1.3-jre"
postgresql = "42.7.0"

[libraries]
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }

[bundles]
spring-stack = ["spring-boot-starter-web"]

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
kotlin
// build.gradle.kts
plugins {
    java
    alias(libs.plugins.spring.boot)
}

dependencies {
    implementation(libs.spring.boot.starter.web)
    implementation(libs.guava)

    runtimeOnly(libs.postgresql)

    testImplementation(libs.junit.jupiter)
    testImplementation(libs.spring.boot.starter.test)
}

6.9 章末小结

                    ★ 第 6 章核心知识图谱 ★

        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
   ┌────▼────┐          ┌────▼────┐          ┌────▼─────┐
   │ 仓库     │          │ 配置     │          │ 冲突解决 │
   ├──────────┤          ├──────────┤          ├──────────┤
   │ Central │          │ implementation│      │ 最高版本 │
   │ Google  │          │ api          │      │ force    │
   │ 私服    │          │ compileOnly  │      │ exclude  │
   │ 镜像    │          │ runtimeOnly  │      │ platform │
   │         │          │ testImpl     │      │ lockfile │
   └─────────┘          └──────────────┘      └──────────┘

                                       ★ 面试必考 ★

🎤 6.10 章末面试题(10 道高频题)

Q1. implementationapi 有什么本质区别?

唯一区别是"传递性"

  • implementation:依赖对消费者不可见(不传递)。你的项目能用,依赖你的人不能用。
  • api:依赖对消费者可见(传递)。你的项目能用,依赖你的人也能用。

怎么选

  • 你库的公开方法签名用了某个依赖的类型(参数 / 返回值)→ 用 api
  • 否则 → 用 implementation(默认推荐,编译更快)

⚠️ api 只有 java-library 插件才有,java 插件没有这个配置。


Q2. compileOnlyruntimeOnly 各自什么场景用?

  • compileOnly:编译期需要、运行期由别人提供。例:

    • Servlet API(运行时由 Tomcat 提供)
    • Lombok(编译完代码就不需要了)
    • @Nullable / @NotNull 等注解工具
  • runtimeOnly:编译期不需要、运行期需要。例:

    • JDBC 驱动(代码只 import java.sql.*,驱动靠运行时反射加载)
    • 日志实现:你 implementation("slf4j-api"),但 runtimeOnly("logback-classic")

Q3. Gradle 怎么解决依赖版本冲突?

默认策略:最高版本胜出

具体流程:

  1. 解析所有 transitive 依赖,构建依赖图;
  2. 对每个 group:artifact,找出所有出现的版本;
  3. 最高版本作为最终选定版本;
  4. 所有依赖该 module 的引用都升级到这个版本。

可以用 ./gradlew dependencyInsight --dependency xxx 看具体仲裁过程。

自定义策略

  • resolutionStrategy.force(...) 强制版本
  • version { strictly(...) } 严格锁定
  • version { reject(...) } 拒绝某些版本

Q4. 如何排除某个传递依赖?

:3 种粒度:

kotlin
// 1. 单个依赖排除
implementation("spring-core") {
    exclude(group = "commons-logging", module = "commons-logging")
}

// 2. 整个 configuration 全局排除
configurations.all {
    exclude(group = "commons-logging", module = "commons-logging")
}

// 3. 排除单个 artifact 但保留依赖
implementation("foo:bar:1.0") {
    exclude(group = "*", module = "specific-module")
}

典型场景:spring-boot 默认拉了 logback,你想换成 log4j2 → 排除 logback 再加 log4j2。


Q5. 什么是 platform / BOM?什么时候用?

Platform = "一组依赖的版本约束清单"(等价 Maven 的 Bill of Materials BOM)。

作用:让你不用为每个相关依赖写版本号,而是一次性定义"这堆库都用 X.Y.Z"。

kotlin
dependencies {
    implementation(platform("org.springframework:spring-framework-bom:6.1.1"))
    implementation("org.springframework:spring-core")    // 无版本,由 platform 决定
    implementation("org.springframework:spring-web")
}

何时用

  • Spring 全家桶(spring-framework-bom)
  • AWS SDK(aws-bom)
  • Jackson(jackson-bom)
  • 任何"一组库要版本对齐"的场景

enforcedPlatform 是更严格的 platform —— 任何冲突直接报错而不是仲裁。少用,因为会丧失灵活性。


Q6. ./gradlew dependencies 输出里 -> 是啥意思?

:表示依赖被仲裁后版本变化了。例如:

+--- module-a:1.0
|    \--- com.google.guava:guava:30.0 -> 32.0.0-jre
+--- module-b:2.0
     \--- com.google.guava:guava:32.0.0-jre

30.0 -> 32.0.0-jre 表示 module-a 原本想要 30.0,但因为 module-b 要 32.0.0-jre,Gradle 选了最高版本 32.0.0-jre,所以 module-a 实际拿到的也是 32.0.0-jre。


Q7. pluginManagementdependencyResolutionManagement 在 settings.gradle.kts 里分别管什么?

  • pluginManagement.repositories:管 plugins 块 里声明的插件去哪儿找。
    kotlin
    pluginManagement {
        repositories { gradlePluginPortal(); google() }
    }
  • dependencyResolutionManagement.repositories:管 dependencies 块 里的依赖去哪儿找。
    kotlin
    dependencyResolutionManagement {
        repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
        repositories { mavenCentral() }
    }

为啥分开?因为插件本身就是 jar,先于 build 脚本被解析;普通依赖在 build 脚本执行时解析。两套机制独立。


Q8. 如何让多个子模块依赖完全可重现?

:用 Dependency Locking

kotlin
// 根 build.gradle.kts
allprojects {
    dependencyLocking {
        lockAllConfigurations()
    }
}

然后跑:

bash
$ ./gradlew dependencies --write-locks

会在每个模块下生成 gradle.lockfile,记录所有 transitive 依赖的精确版本。提交到 Git 后,未来构建必须严格匹配 lockfile,否则报错。

类似:npm 的 package-lock.json、Pipenv 的 Pipfile.lock、Cargo 的 Cargo.lock、yarn 的 yarn.lock


Q9. 如何看一个依赖是怎么进入项目的?

:用 dependencyInsight 命令:

bash
$ ./gradlew dependencyInsight --dependency guava --configuration runtimeClasspath

输出会列出:

  • 这个依赖的最终选定版本
  • 仲裁原因(被谁强制升级 / 锁定)
  • 完整依赖路径(谁拉了它)

这是排查"为什么我的项目里突然多了这个 jar"的主要手段。


Q10. Maven Scope 和 Gradle Configuration 怎么对应?

Maven ScopeGradle Configuration
compile(已废弃)implementation(替代品)
compile + 暴露api(需 java-library 插件)
providedcompileOnly
runtimeruntimeOnly
testtestImplementation
system(极少用)compileOnly + files()
import(仅 dependencyManagement)platform() / enforcedPlatform()

💡 一个常见错误:把 Maven 的 compile 直接对应到 Gradle 的 compile(已删除),其实应该用 implementationapi根据是否暴露给消费者决定


下一章 → 第 7 章 · Plugin 插件体系 →

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

txt
/*
 * 第 6 章 · 依赖管理演示 — build.gradle.kts
 *
 * 跑下面这些命令查看效果:
 *   ./gradlew dependencies
 *   ./gradlew dependencies --configuration runtimeClasspath
 *   ./gradlew dependencyInsight --dependency guava
 *   ./gradlew showDeps             —— 自定义任务,打印依赖类型
 */

plugins {
    `java-library`              // 必须用 java-library 才能用 api
}

group = "com.example.gradle.dep"
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {
    // ===== 1. implementation:编译+运行可见,不传递 =====
    implementation("com.google.guava:guava:32.1.3-jre")
    implementation("org.slf4j:slf4j-api:2.0.9")

    // ===== 2. api:传递给消费者 =====
    api("org.apache.commons:commons-lang3:3.13.0")

    // ===== 3. compileOnly:仅编译需要 =====
    compileOnly("org.projectlombok:lombok:1.18.30")
    annotationProcessor("org.projectlombok:lombok:1.18.30")

    // ===== 4. runtimeOnly:仅运行需要 =====
    runtimeOnly("ch.qos.logback:logback-classic:1.4.11")

    // ===== 5. testImplementation =====
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    testImplementation("org.assertj:assertj-core:3.24.2")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // ===== 6. 排除传递依赖示例 =====
    implementation("org.springframework:spring-context:6.1.1") {
        exclude(group = "commons-logging", module = "commons-logging")
    }

    // ===== 7. platform / BOM 演示 =====
    // implementation(platform("org.springframework:spring-framework-bom:6.1.1"))
    // implementation("org.springframework:spring-web")  // 不用写版本

    // ===== 8. 强制版本演示 =====
    // implementation("com.google.guava:guava") {
    //     version { strictly("30.0") }
    // }
}

// ===== 9. 全局 force / exclude =====
configurations.all {
    // resolutionStrategy.force("com.google.guava:guava:32.1.3-jre")
    exclude(group = "junit", module = "junit")  // 排除老 JUnit 4 的传递依赖
}

// ===== 10. 自定义任务:打印依赖统计 =====
tasks.register("showDeps") {
    group = "ch06-demo"
    description = "打印各 configuration 的依赖数"
    doLast {
        listOf(
            "implementation",
            "api",
            "compileOnly",
            "runtimeOnly",
            "testImplementation",
            "compileClasspath",
            "runtimeClasspath",
            "testRuntimeClasspath"
        ).forEach { name ->
            val cfg = configurations.findByName(name)
            if (cfg != null) {
                val direct = cfg.dependencies.size
                val resolved = if (cfg.isCanBeResolved) cfg.resolvedConfiguration.firstLevelModuleDependencies.size else -1
                println(String.format("%-25s direct=%2d  resolved-direct=%s",
                    name, direct, if (resolved >= 0) resolved.toString() else "(non-resolvable)"))
            }
        }
    }
}

tasks.test {
    useJUnitPlatform()
}
txt
rootProject.name = "ch06-dependency-demo"

dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        // 国内镜像加速(可选)
        // maven("https://maven.aliyun.com/repository/public")
        mavenCentral()
    }
}

build.gradle.kts ↗ · settings.gradle.kts ↗