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.ktsdependencyResolutionManagement 里。这样所有子模块统一管理,不会出现「这个模块用了那个仓库,那个模块没用」的混乱局面。

// 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 或者重新生成。

我的习惯:在 CI 构建时加上 --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 知识体系总览

下面这张图总结了本章的核心逻辑:

依赖管理 仓库配置 Maven Central Google / JitPack 声明依赖 implementation / api compileOnly / runtimeOnly 版本控制 依赖约束 强制版本 锁定与分析 依赖锁定 依赖树分析

依赖管理说白了就四件事:去哪找(仓库)、怎么声明(配置项)、版本怎么定(约束/强制)、怎么保证一致性(锁定)。把这四件事理清楚了,构建脚本的依赖部分基本不会出大问题。

嗯,今天就聊到这。记住一点:依赖管理不是「写一行代码就完事」的活。它需要你理解背后的机制,才能在出问题时快速定位。


公众号:蓝海资料掘金营,微信deep3321