6、依赖管理:仓库配置、依赖声明与配置、依赖传递与排除、动态版本与版本锁定
依赖管理,说白了就是 Gradle 帮你搞定「你的代码需要哪些第三方库」这件事。我刚开始从 Maven 转到 Gradle 时,觉得这玩意儿不就是换个写法嘛。后来踩了几个坑才发现,Gradle 的依赖管理远比我想象的灵活,也远比我想象的容易翻车。
这一章,我把依赖管理拆成四个核心话题来讲:仓库怎么配、依赖怎么写、传递依赖怎么控制、版本怎么管。每个点我都会结合自己项目里的真实经历,帮你避开那些我当年踩过的坑。
6.1 仓库配置:你的依赖从哪来
Gradle 找依赖,就像你去超市买东西。超市就是仓库(Repository)。你得告诉 Gradle 去哪几个超市逛。
最常见的配置长这样:
repositories {
mavenCentral()
// 或者用国内镜像
maven { url 'https://maven.aliyun.com/repository/public' }
// 私有仓库
maven { url 'https://nexus.yourcompany.com/repository/maven-public/' }
}
我个人习惯把仓库顺序排好。Gradle 会按顺序查找,找到第一个就停下。所以快的、稳定的仓库放前面。
还有一种情况——你公司内部有私有构件库(比如 Nexus 或 Artifactory)。这时候需要配认证信息:
repositories {
maven {
url 'https://nexus.yourcompany.com/repository/maven-private/'
credentials {
username = 'your-username'
password = 'your-password'
}
}
}
6.2 依赖声明与配置:implementation 还是 api?
声明依赖很简单,但选对配置项才是关键。Gradle 提供了好几种依赖配置,最常用的就是 implementation 和 api。
| 配置项 | 作用 | 是否暴露给消费者 |
|---|---|---|
| implementation | 编译和运行时需要,但不暴露给依赖方 | 否 |
| api | 编译和运行时需要,且暴露给依赖方 | 是 |
| compileOnly | 仅编译时需要,运行时不需要 | 否 |
| runtimeOnly | 仅运行时需要,编译时不需要 | 否 |
| testImplementation | 仅测试时需要 | 否 |
举个例子:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.0'
api 'com.google.guava:guava:33.0.0-jre'
compileOnly 'org.projectlombok:lombok:1.18.30'
runtimeOnly 'mysql:mysql-connector-java:8.0.33'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
你想想看,如果滥用 api,会导致依赖传递链越来越长,编译时间越来越慢。我有个项目,一开始图省事全用 api,结果一个简单的改动要重新编译半个项目。后来改成 implementation,构建速度快了将近 40%。
6.3 依赖传递与排除:别让依赖「打架」
Gradle 默认会拉取传递依赖。就是说,你引入 A,A 依赖 B,B 依赖 C,那么 C 也会被自动拉进来。
听起来很方便对吧?但问题来了——版本冲突。A 依赖 C 1.0,B 依赖 C 2.0,到底用哪个?
Gradle 的策略是:选版本最高的那个。但这不一定是你想要的。
我曾经在一个微服务项目里遇到过:引入的日志框架 A 依赖了旧版 Jackson,而业务代码需要新版 Jackson。结果运行时疯狂报错,排查了两天才发现是传递依赖搞的鬼。
解决方案有两种:
- 排除传递依赖
- 强制指定版本
排除写法:
implementation('com.example:some-library:1.0.0') {
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind'
}
或者全局排除:
configurations.all {
exclude group: 'commons-logging', module: 'commons-logging'
}
强制指定版本:
configurations.all {
resolutionStrategy {
force 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
}
}
6.4 动态版本与版本锁定:稳定压倒一切
Gradle 支持动态版本声明,比如:
implementation 'com.google.guava:guava:30.+'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.+'
这样每次构建都会拉取最新的小版本。听起来很美好,对吧?
但我在生产环境吃过亏。有一次,Guava 从 30.1 升级到 30.2,修复了一个 bug,但也引入了一个新的 API 变更。结果我们的代码在 30.1 上跑得好好的,升级后直接编译失败。关键是——没人注意到版本变了,直到 CI 构建失败才发现。
所以我的建议是:开发环境可以用动态版本尝鲜,但生产环境必须锁定版本。
Gradle 提供了版本锁定的机制:
// 在 gradle.properties 或 build.gradle 中
guava.version=33.0.0-jre
jackson.version=2.15.2
// 引用
implementation "com.google.guava:guava:${guava.version}"
implementation "com.fasterxml.jackson.core:jackson-databind:${jackson.version}"
更专业的做法是用 Gradle 的 依赖锁定文件:
// 在 build.gradle 中启用
dependencyLocking {
lockAllConfigurations()
}
// 生成锁定文件
// 运行: gradle dependencies --write-locks
锁定文件会记录所有依赖的确切版本。之后构建时,Gradle 会严格按照锁定文件来拉取依赖,不会因为远程仓库更新而改变。
还有一种方式是用 BOM(Bill of Materials)。Spring Boot 的依赖管理就是基于 BOM 的:
implementation platform('org.springframework.boot:spring-boot-dependencies:3.2.0')
implementation 'org.springframework.boot:spring-boot-starter-web'
// 不需要写版本号,由 BOM 统一管理
BOM 的好处是:所有依赖版本由平台统一维护,你只管写业务代码。我个人非常推荐这种方式,尤其是团队项目。
依赖管理这件事,说简单也简单,说复杂也复杂。核心就一句话:明确你要什么,控制你得到什么,锁定你用什么。 做到这三点,你的构建就不会在依赖上翻车。