在多模块的 Gradle 项目中,随着时间的推移,依赖管理往往会变得比较复杂。我注意到不同的子模块引用的第三方库版本容易出现不一致的情况,有时还会产生依赖冲突。

过去,我曾尝试过通过 ext 扩展属性(或 gradle.properties)加字符串拼接,或是使用 buildSrc 编写 Kotlin 脚本来集中管理版本。但它们都有一些局限性,例如缺乏代码提示,或者在 buildSrc 中修改一行代码会导致整个项目缓存失效重新编译。

后来我了解到,从 Gradle 7.4 开始,官方引入了 Gradle Version Catalog (版本目录)(即 libs.versions.toml)。最近,我尝试将一个项目迁移到了这种依赖管理方式,并在此记录一下我的实践过程。

什么是 Version Catalog?

我的理解是,Version Catalog 允许将所有的版本号、依赖库坐标(GAV)以及 Gradle 插件统一声明在一个独立的 .toml 文件中。然后在各个模块的 build.gradle / build.gradle.kts 中,可以以类型安全的方式去引用它们。

我观察到的优势:

  1. 统一控制源:全项目的依赖都集中在一个物理文件中,便于查阅和修改。
  2. IDE 体验:较好地支持了 IntelliJ IDEA 的代码补全和版本升级提示。
  3. 性能友好:相比 buildSrc 方案,修改 .toml 文件通常不会导致整个 Gradle 构建逻辑的缓存失效。
  4. Bundle(依赖包)机制:可以将相关的几个库打包在一起,统一引入。

迁移过程记录

我以一个基于 Kotlin DSL (build.gradle.kts) 的 Spring Boot 多模块项目为例,记录了我的迁移步骤。

第一步:创建 libs.versions.toml

我在项目根目录下的 gradle 文件夹中,新建了一个名为 libs.versions.toml 的文件。

这个文件主要分为四个区块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# gradle/libs.versions.toml

[versions]
# 集中定义所有版本号
spring-boot = "4.0.3"
jjwt = "0.12.3"

[libraries]
# JWT 相关
jjwt-api = { group = "io.jsonwebtoken", name = "jjwt-api", version.ref = "jjwt" }
jjwt-impl = { group = "io.jsonwebtoken", name = "jjwt-impl", version.ref = "jjwt" }

[bundles]
# 把经常一起出现的依赖打个包
jwt = ["jjwt-api", "jjwt-impl"]

[plugins]
# 定义 Gradle 插件
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }

第二步:清理旧的属性定义

我移除了以前在 gradle.properties 或者根 build.gradle.kts 里定义过的零散版本号(如 springBootVersion=4.0.3)。

第三步:改造根项目(插件声明与版本提取)

在根目录的 build.gradle.kts 中,我将原有的字符串插件声明改为了 alias() 的方式。

修改前:

1
2
3
4
plugins {
id("org.springframework.boot") version "4.0.3" apply false
id("com.diffplug.spotless") version "8.2.1" apply false
}

修改后:

1
2
3
4
plugins {
alias(libs.plugins.spring.boot) apply false
alias(libs.plugins.spotless) apply false
}

如果在子模块闭包 subprojects { ... } 中需要用到版本号,我发现可以提前在闭包外提取出来:

1
2
3
4
5
6
// 提前提取版本号供内部使用
val springBootVersion = libs.versions.spring.boot.get()

subprojects {
// 这里可以直接使用 springBootVersion
}

第四步:改造子模块依赖声明

进入各个子模块后,我将硬编码的依赖替换为了 Catalog 引用。我注意到在 toml 文件中使用的 -(横杠)在 Kotlin DSL 中会被自动转换为 .(点)。

修改前:

1
2
3
4
5
dependencies {
implementation("cn.hutool:hutool-all:5.8.34")
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
implementation("io.jsonwebtoken:jjwt-impl:0.12.3")
}

修改后:

1
2
3
4
5
6
7
dependencies {
// 单个依赖
implementation(libs.hutool.all)

// 使用 bundle 一键引入所有 JWT 依赖
implementation(libs.bundles.jwt)
}

排查与学习:进阶技巧

  1. 与 Spring Dependency Management 的配合
    对于使用了 Spring BOM 的项目,我倾向于采用 Version Catalog + BOM 的组合。让 BOM 去管理它内置的框架依赖(如 spring-boot-starter-web, jackson),剩余的第三方库再用 toml 统一管理。

  2. 命名规范
    toml 文件中,我发现在命名时使用短横线 - 分隔单词(如 spring-boot)比较好,因为 Gradle 在生成访问器时会自动将其转换为点号形式(libs.versions.spring.boot),在代码中引用时较为自然。

  3. 自动化升级尝试
    由于 toml 是标准的配置文件格式,我尝试了将其与自动化工具(如 Dependabot 或 Renovate)结合。发现新版本发布时,工具可以直接解析并修改 .toml 文件,这使得依赖升级变得更加自动化。

总结反思

这次将项目迁移到 Gradle Version Catalog 的实践,帮助我更好地理解了现代 Gradle 的依赖管理机制。虽然初次迁移需要花费一些时间,但完成后确实提升了构建脚本的整洁度和类型安全性。这也提醒了我需要进一步了解 Gradle 缓存机制等更深层次的技术细节。