4. Gradle依赖管理
依赖管理这东西,说白了就是帮你的项目"要啥有啥",但又不能"要啥来啥"乱成一锅粥。我个人觉得,仓库配置和依赖声明是基础,传递依赖和冲突解决才是真正考验人的地方。
我记得几年前接手一个老项目,build.gradle里密密麻麻全是依赖,版本号写得到处都是。一执行build,控制台就飘红,全是Conflict。那会儿我就意识到,版本锁定这步,真不能偷懒。
4.1 仓库配置
仓库,就是依赖的"货源"。Gradle支持多种仓库类型。我个人习惯在项目开始前,先把repositories块统一配置好。
repositories {
mavenCentral() // 中央仓库,最常用
google() // Android专用,Google的Maven仓库
jcenter() // 已弃用,但老项目里还能见到
maven { url "https://jitpack.io" } // 第三方仓库,比如GitHub上的库
flatDir { dirs 'libs' } // 本地libs文件夹
}
这里有个细节:仓库顺序会影响依赖解析速度。我一般把mavenCentral()放前面,因为它最稳定。为什么?你想想看,Gradle是按顺序去每个仓库找依赖的,找到了就停。如果有个慢仓库放前面,每次构建都得等半天。
mavenLocal()放第一位,结果团队里某个人本地缓存了个旧版本的库,整个构建就全歪了。后来我改成只保留mavenCentral()和google(),把本地仓库去掉,才消停。
4.2 依赖声明
依赖声明,就是告诉Gradle:"我需要这个,那个,还有那个"。Gradle里常用的配置项有这么几种:
| 配置项 | 作用 | 传递性 | 使用场景 |
|---|---|---|---|
implementation |
编译时和运行时都可用 | 不对外暴露 | 大部分内部依赖,推荐使用 |
api |
编译时和运行时都可用,且对外暴露 | 对外暴露 | Library模块的公开接口依赖 |
compileOnly |
仅编译时可用 | 不传递 | 编译期注解处理器、Lombok等 |
runtimeOnly |
仅运行时可用 | 不传递 | JDBC驱动等 |
testImplementation |
仅测试编译和运行时 | 不传递 | JUnit、Mockito等测试框架 |
这里我重点想聊的是implementation和api的区别。说白了,implementation就是"我用,你别管";api就是"我用,你也用"。我在项目中遇到过有人把所有依赖都写成api,结果模块A改了某个依赖版本,模块B、C全部得重新编译,构建时间直接翻倍。
dependencies {
// 推荐:用implementation把依赖藏起来
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
// 只有对外暴露接口的模块才用api
api 'com.google.code.gson:gson:2.8.8'
// 编译期用的,比如Lombok
compileOnly 'org.projectlombok:lombok:1.18.24'
// 测试专用
testImplementation 'junit:junit:4.13.2'
}
testImplementation写成了implementation,结果生产环境打出来的jar包里混进了JUnit的类,部署上去直接报错。从那以后,我每次写完dependencies都要跑一遍gradle dependencies确认范围。
4.3 传递依赖与冲突解决
传递依赖,就是你引入的库A,它自己又拉了库B和库C。Gradle默认会把这些"孙子"依赖也带进来。这本来是好事,但问题在于:不同库可能拉了同一个库的不同版本。
举个例子:你引入了com.google.guava:guava:30.0,而引入的com.squareup.okhttp3:okhttp:4.9.3又依赖com.google.guava:guava:28.0。这时候版本冲突就来了。
Gradle处理冲突的规则是:默认采用最高版本。但有时候最高版本不一定兼容。这时候就需要我们手动介入了。
configurations.all {
resolutionStrategy {
// 强制使用某个版本
force 'com.google.guava:guava:30.1-android'
// 如果出现冲突,优先选择这个模块的依赖
preferProjectModules()
}
// 排除某个传递依赖
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
还有一个常用的方法,就是直接在被依赖的声明里排除:
implementation('com.squareup.okhttp3:okhttp:4.9.3') {
// 排除它内部的某个传递依赖
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
}
我在项目中遇到过最头疼的一次:A库依赖了B库1.0版,C库依赖了B库2.0版,而B库1.0和2.0的API完全不兼容。最后我只能用force锁死版本,然后手动测试所有功能是否正常。嗯,这里要注意,force虽然好用,但一定要写测试用例,否则可能偷跑出去一个不兼容的API调用。
4.4 版本锁定
版本锁定,就是把你当前构建时实际用到的所有依赖版本,记录到一个文件里。这样下次构建,不管谁执行,都能保证用完全一样的版本。
Gradle官方推荐的做法是配合Gradle构建缓存和依赖锁定插件。不过我这里说个更常用的方法:gradle.lockfile。
// 在settings.gradle里开启依赖锁定
dependencyResolutionManagement {
repositories.mavenCentral()
lockMode.set(LockMode.STRICT) // 严格模式,必须存在lockfile才能构建
}
然后执行:
./gradlew dependencies --write-locks
这会生成一个gradle.lockfile文件,记录了所有依赖的精确版本。这个文件应该提交到版本管理里,团队里每个人都用同一份。
我个人的习惯是:每次添加或更新依赖后,都重新生成一下lockfile。而且,我会在CI流水线里加一个步骤,检查lockfile是否有变更。如果没更新lockfile就提交,直接构建失败。
4.5 知识结构图
下面这张图,把Gradle依赖管理的核心流程串起来了。从仓库配置开始,到依赖声明、传递解析、冲突处理,最后锁定版本,环环相扣。