主题
第 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()
}
}
}把 nexusUser 和 nexusPass 写到 ~/.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)
- JDBC 驱动(你代码只 import
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 runtimeClasspath6.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. implementation 和 api 有什么本质区别?
答:唯一区别是"传递性"。
implementation:依赖对消费者不可见(不传递)。你的项目能用,依赖你的人不能用。api:依赖对消费者可见(传递)。你的项目能用,依赖你的人也能用。
怎么选:
- 你库的公开方法签名用了某个依赖的类型(参数 / 返回值)→ 用
api - 否则 → 用
implementation(默认推荐,编译更快)
⚠️
api只有java-library插件才有,java插件没有这个配置。
Q2. compileOnly 和 runtimeOnly 各自什么场景用?
答:
compileOnly:编译期需要、运行期由别人提供。例:- Servlet API(运行时由 Tomcat 提供)
- Lombok(编译完代码就不需要了)
@Nullable/@NotNull等注解工具
runtimeOnly:编译期不需要、运行期需要。例:- JDBC 驱动(代码只
import java.sql.*,驱动靠运行时反射加载) - 日志实现:你
implementation("slf4j-api"),但runtimeOnly("logback-classic")
- JDBC 驱动(代码只
Q3. Gradle 怎么解决依赖版本冲突?
答:默认策略:最高版本胜出。
具体流程:
- 解析所有 transitive 依赖,构建依赖图;
- 对每个 group:artifact,找出所有出现的版本;
- 选最高版本作为最终选定版本;
- 所有依赖该 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-jre30.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. pluginManagement 和 dependencyResolutionManagement 在 settings.gradle.kts 里分别管什么?
答:
pluginManagement.repositories:管 plugins 块 里声明的插件去哪儿找。kotlinpluginManagement { repositories { gradlePluginPortal(); google() } }dependencyResolutionManagement.repositories:管 dependencies 块 里的依赖去哪儿找。kotlindependencyResolutionManagement { 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 Scope | Gradle Configuration |
|---|---|
compile(已废弃) | implementation(替代品) |
compile + 暴露 | api(需 java-library 插件) |
provided | compileOnly |
runtime | runtimeOnly |
test | testImplementation |
system(极少用) | compileOnly + files() |
import(仅 dependencyManagement) | platform() / enforcedPlatform() |
💡 一个常见错误:把 Maven 的
compile直接对应到 Gradle 的compile(已删除),其实应该用implementation或api,根据是否暴露给消费者决定。
下一章 → 第 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()
}
}