8、依赖管理:仓库配置、声明依赖、依赖约束与强制版本
依赖管理,说白了就是告诉 Gradle:「我要用哪些库,从哪下载,版本怎么控制」。
我刚开始用 Kotlin DSL 写构建脚本时,觉得这不就是换个语法写 dependencies 嘛。后来踩了几个坑才发现——这里面的门道比想象中深得多。
8.1 仓库配置:告诉 Gradle 去哪找依赖
Gradle 本身不存任何第三方库。它得去仓库里拉。仓库就是存放 jar/aar 的地方。
常见的仓库有三种:
- Maven Central:老牌公共仓库,啥都有,但偶尔抽风
- Google:Android 开发必备,放 AndroidX、Compose 等
- JitPack / 私有仓库:公司内部或 GitHub 上的库
配置方式很简单,在 repositories 块里声明:
repositories {
mavenCentral()
google()
maven { url = uri("https://jitpack.io") }
// 私有仓库,需要认证
maven {
url = uri("https://nexus.company.com/repository/maven-releases/")
credentials {
username = "admin"
password = "pass123"
}
}
}
我个人习惯把仓库写在 settings.gradle.kts 的 dependencyResolutionManagement 里。这样所有子模块统一管理,不会出现「这个模块用了那个仓库,那个模块没用」的混乱局面。
// settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
google()
}
}
RepositoriesMode.FAIL_ON_PROJECT_REPOS 可以强制所有子模块只能用这里声明的仓库。谁偷偷在 build.gradle.kts 里加仓库?直接编译报错。
8.2 声明依赖:implementation、api、compileOnly 怎么选
依赖声明看起来就是一行代码,但配置项选错了,编译时间、包体积都会受影响。
Kotlin DSL 里声明依赖的典型写法:
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
api("com.google.guava:guava:33.0.0-jre")
compileOnly("org.projectlombok:lombok:1.18.30")
runtimeOnly("ch.qos.logback:logback-classic:1.4.14")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
}
这几个配置项的区别,我整理了一张表:
| 配置项 | 作用范围 | 是否传递 | 典型场景 |
|---|---|---|---|
implementation |
当前模块 + 运行时 | 否 | 大部分内部依赖,编译快 |
api |
当前模块 + 依赖方 + 运行时 | 是 | 暴露给外部使用的接口库 |
compileOnly |
仅编译期 | 否 | 注解处理器、编译期工具 |
runtimeOnly |
仅运行时 | 否 | 日志实现、数据库驱动 |
testImplementation |
仅测试编译 + 测试运行 | 否 | JUnit、Mockito |
implementation 就别用 api。api 会把依赖暴露给调用方,导致编译时间变长。我见过一个项目把所有依赖都写成 api,结果改一个库版本,全项目重新编译——那叫一个酸爽。
8.3 依赖约束:解决版本冲突的优雅方式
项目大了以后,版本冲突是家常便饭。比如 A 库依赖 Guava 30.0,B 库依赖 Guava 31.0。Gradle 默认会选最高版本,但有时候会出问题。
依赖约束(Dependency Constraints)就是用来解决这个的。它有点像「温柔的版本锁定」——不强制,但建议用某个版本。
dependencies {
constraints {
implementation("com.google.guava:guava:33.0.0-jre") {
because("修复了 CVE-2023-XXXX 安全漏洞")
}
implementation("com.squareup.okhttp3:okhttp:4.12.0") {
because("统一团队使用的版本")
}
}
}
约束的好处是:如果某个传递依赖已经满足版本要求,它不会强行覆盖。只有版本冲突时,约束才会生效。
我曾经在一个多模块项目里,每个子模块都声明了自己的 Guava 版本。后来统一用 constraints 管理,再也不用担心「这个模块用了 30.0,那个模块用了 31.0」的问题了。
8.4 强制版本:当温柔不管用时
依赖约束是「建议」,但有些场景下你需要「命令」。比如某个库有严重 bug,必须用特定版本。
这时候用 force = true:
dependencies {
implementation("com.google.guava:guava:33.0.0-jre") {
version {
strictly("33.0.0-jre")
}
}
}
或者更简洁的写法:
implementation("com.google.guava:guava:33.0.0-jre!!")
两个感叹号 !! 就是「强制版本」的语法糖。Gradle 看到它,会直接覆盖所有传递依赖的版本。
!!,结果升级一个库时,整个依赖树炸了——因为强制版本和实际需要的版本不兼容。
我的建议是:先用约束(constraints),解决不了再用强制。强制版本只用于「这个版本有 bug,必须用那个版本」的紧急情况。
8.5 依赖锁定:让构建可复现
你有没有遇到过这种情况:今天构建没问题,明天同样的代码构建就报错了?
原因往往是依赖版本变了——某个库发布了新版本,Gradle 自动拉到了新版本。
依赖锁定(Dependency Locking)就是解决这个的。它会生成一个 gradle.lockfile,记录所有依赖的精确版本。
启用方式:
// build.gradle.kts
dependencyLocking {
lockAllConfigurations()
}
然后执行:
./gradlew dependencies --write-locks
生成的 lockfile 长这样:
# This is a Gradle generated file for dependency locking.
com.google.guava:guava:33.0.0-jre=compileClasspath,runtimeClasspath
com.squareup.okhttp3:okhttp:4.12.0=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.9.22=compileClasspath,runtimeClasspath
把这个文件提交到 Git 里,团队所有人都用完全相同的版本构建。想升级?手动改 lockfile 或者重新生成。
--locked 参数。如果 lockfile 和实际依赖不一致,构建直接失败。这样能第一时间发现「谁偷偷改了依赖没更新 lockfile」。
8.6 依赖分析:用 Gradle 命令看清依赖树
依赖出问题时,别瞎猜。用 Gradle 提供的命令看看依赖树:
./gradlew :app:dependencies
输出结果会展示所有依赖的层级关系:
compileClasspath - Compile classpath for source set 'main'.
+--- com.squareup.okhttp3:okhttp:4.12.0
| +--- com.squareup.okio:okio:3.6.0
| | \--- com.squareup.okio:okio-jvm:3.6.0
| \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.22
\--- com.google.guava:guava:33.0.0-jre
+--- com.google.guava:failureaccess:1.0.2
\--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
看到 -> 符号了吗?它表示版本冲突后 Gradle 选择了哪个版本。比如:
com.google.guava:guava:30.0-jre -> 33.0.0-jre
意思是「请求的是 30.0,但实际用了 33.0」。
我排查依赖冲突时,第一件事就是跑 dependencies 任务。比翻代码快多了。
8.7 知识体系总览
下面这张图总结了本章的核心逻辑:
依赖管理说白了就四件事:去哪找(仓库)、怎么声明(配置项)、版本怎么定(约束/强制)、怎么保证一致性(锁定)。把这四件事理清楚了,构建脚本的依赖部分基本不会出大问题。
嗯,今天就聊到这。记住一点:依赖管理不是「写一行代码就完事」的活。它需要你理解背后的机制,才能在出问题时快速定位。
公众号:蓝海资料掘金营,微信deep3321