6、依赖管理:仓库配置、依赖声明与配置、依赖传递与排除、动态版本与版本锁定

依赖管理,说白了就是 Gradle 帮你搞定「你的代码需要哪些第三方库」这件事。我刚开始从 Maven 转到 Gradle 时,觉得这玩意儿不就是换个写法嘛。后来踩了几个坑才发现,Gradle 的依赖管理远比我想象的灵活,也远比我想象的容易翻车。

这一章,我把依赖管理拆成四个核心话题来讲:仓库怎么配、依赖怎么写、传递依赖怎么控制、版本怎么管。每个点我都会结合自己项目里的真实经历,帮你避开那些我当年踩过的坑。

Gradle 依赖管理核心体系 仓库配置 依赖声明 传递与排除 版本管理 mavenCentral / jcenter / 私有仓库 implementation / api / compileOnly transitive / exclude / force + / 锁定 / BOM 核心原则 明确声明 → 控制传递 → 锁定版本 → 可复现构建

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 会按顺序查找,找到第一个就停下。所以快的、稳定的仓库放前面。

小技巧: 国内项目建议把阿里云镜像放在 mavenCentral() 前面。我第一次配的时候顺序搞反了,结果每次构建都慢得像蜗牛,后来才发现是仓库顺序的问题。

还有一种情况——你公司内部有私有构件库(比如 Nexus 或 Artifactory)。这时候需要配认证信息:

repositories {
    maven {
        url 'https://nexus.yourcompany.com/repository/maven-private/'
        credentials {
            username = 'your-username'
            password = 'your-password'
        }
    }
}
注意: 千万别把密码硬编码在 build.gradle 里!我曾经见过同事把生产环境的 Nexus 密码直接提交到 Git 仓库,结果被安全团队通报批评。正确的做法是用 gradle.properties 或者环境变量。

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%。

核心原则: 能用 implementation 就别用 api。只有当你确定这个依赖需要被下游模块直接使用时,才考虑 api。

6.3 依赖传递与排除:别让依赖「打架」

Gradle 默认会拉取传递依赖。就是说,你引入 A,A 依赖 B,B 依赖 C,那么 C 也会被自动拉进来。

听起来很方便对吧?但问题来了——版本冲突。A 依赖 C 1.0,B 依赖 C 2.0,到底用哪个?

Gradle 的策略是:选版本最高的那个。但这不一定是你想要的。

我曾经在一个微服务项目里遇到过:引入的日志框架 A 依赖了旧版 Jackson,而业务代码需要新版 Jackson。结果运行时疯狂报错,排查了两天才发现是传递依赖搞的鬼。

解决方案有两种:

  1. 排除传递依赖
  2. 强制指定版本

排除写法:

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'
    }
}
避坑指南: 我曾经用 force 强制指定了一个版本,结果另一个依赖因为这个版本不兼容直接炸了。所以 force 虽好,但一定要确认所有依赖都兼容你指定的版本。建议先用 gradle dependencies 命令查看完整的依赖树,再做决定。

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 会严格按照锁定文件来拉取依赖,不会因为远程仓库更新而改变。

我的习惯: 每次发布前,我会手动执行一次 gradle dependencies --write-locks,然后把生成的 gradle.lockfile 提交到 Git。这样团队所有人都用同一套依赖版本,再也不会出现「我本地能跑,你那边不行」的尴尬情况。

还有一种方式是用 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 的好处是:所有依赖版本由平台统一维护,你只管写业务代码。我个人非常推荐这种方式,尤其是团队项目。


依赖管理这件事,说简单也简单,说复杂也复杂。核心就一句话:明确你要什么,控制你得到什么,锁定你用什么。 做到这三点,你的构建就不会在依赖上翻车。