版本目录:Version Catalog 介绍

说到多模块项目的版本管理,我早期吃过不少苦头。那时候每个模块的 build.gradle 里都散落着各种版本号,升级一个依赖就得全局搜索替换,稍不留神就漏掉一个。后来 Gradle 7.0 推出了 Version Catalog,说白了就是给所有依赖版本号找了个「中央仓库」。

Version Catalog 的核心思想很简单:把依赖的坐标和版本号集中管理。你想想看,一个大型项目几十个模块,每个模块都写一遍 implementation 'com.google.guava:guava:31.1-jre',哪天 Guava 升级了,你得改多少个文件?用 Catalog 之后,只需要改一个地方。

Version Catalog 解决了三个核心痛点:

  • 版本号散落各处,升级困难
  • 不同模块可能引入同一依赖的不同版本,导致冲突
  • 新人接手项目,搞不清哪些依赖是「官方推荐」的

我个人习惯把 Version Catalog 看作是「依赖的清单」。它不负责下载,只负责告诉你:这个项目里,某某依赖应该用哪个版本。真正干活的是 Gradle 的依赖解析引擎。

Version Catalog libs.versions.toml 集中声明版本号与依赖坐标 构建脚本引用 libs.xxx.yyy 语法 多模块统一管理 所有模块共享同一份版本清单 核心逻辑:一处定义,处处使用 升级依赖只需修改一个文件

libs.versions.toml 配置详解

这个文件是 Version Catalog 的「宪法」。它通常放在 gradle/ 目录下,名字固定为 libs.versions.toml。嗯,这里要注意:文件名不能改,位置也不能改,Gradle 会默认去这里找。

文件结构分三块:versionslibrariesbundles。我直接给你看一个真实项目的例子:

# gradle/libs.versions.toml

[versions]
# 核心框架
spring-boot = "3.2.0"
spring-cloud = "2023.0.0"
mybatis-plus = "3.5.5"

# 工具库
guava = "32.1.3-jre"
hutool = "5.8.25"
lombok = "1.18.30"

# 测试
junit = "5.10.1"
mockito = "5.7.0"

[libraries]
# 格式:别名 = { module = "group:artifact", version.ref = "版本引用" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot" }
mybatis-plus-spring-boot3 = { module = "com.baomidou:mybatis-plus-spring-boot3-starter", version.ref = "mybatis-plus" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
hutool-all = { module = "cn.hutool:hutool-all", version.ref = "hutool" }
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }

# 也可以直接写死版本号(不推荐)
# fastjson = { module = "com.alibaba:fastjson", version = "2.0.45" }

[bundles]
# 依赖包:一组相关依赖的集合
spring-web = ["spring-boot-starter-web", "spring-boot-starter-test"]
testing = ["junit", "mockito"]

我的经验之谈:versions 块里版本号尽量用字符串,别用数字。我见过有人写 version = 3.2.0,结果 Gradle 把它当浮点数解析成 3.2,直接翻车。另外,libraries 块的别名建议用「-」连接,比如 spring-boot-starter-web,这样在构建脚本里引用时更自然。

这里有个细节:version.ref 引用的是 [versions] 里定义的键名。如果你某个依赖版本比较特殊,也可以直接在 libraries 里写死 version,但我个人不建议这么干——既然用了 Catalog,就统一管理到底。

在构建脚本中使用目录

配置好 toml 文件后,怎么在 build.gradle 里用呢?Gradle 会自动生成一个 libs 扩展对象,你直接点就行了。看代码:

// build.gradle.kts(Kotlin DSL)
dependencies {
    implementation(libs.spring.boot.starter.web)
    implementation(libs.guava)
    implementation(libs.hutool.all)
    
    // 使用 bundle
    implementation(libs.bundles.testing)
    
    // 编译期注解处理器
    annotationProcessor(libs.lombok)
}

// 如果是 Groovy DSL,语法略有不同
// build.gradle
dependencies {
    implementation libs.spring.boot.starter.web
    implementation libs.guava
    implementation libs.bundles.testing
}

你看,是不是很清爽?不用再写长长的 group:artifact:version 了。而且 IDE 会有自动补全,你输入 libs. 之后,所有可用的依赖都会列出来。

我曾经踩过的坑:一开始我把 toml 文件里的别名写成驼峰式,比如 springBootStarterWeb。结果在 Kotlin DSL 里引用时,libs.springBootStarterWeb 能工作,但 Groovy DSL 里死活找不到。后来查文档才发现,Gradle 会把别名中的「-」和「.」都映射成方法调用,所以统一用「-」最保险。

另外,如果你在子模块里想用 Catalog,不需要额外配置。只要根项目的 settings.gradle.kts 里启用了 Version Catalog(默认就启用),所有子模块都能直接使用 libs 对象。

多模块统一版本管理

这才是 Version Catalog 真正发力的场景。我参与过一个电商项目,有 20 多个子模块,每个模块都依赖 Spring Boot、MyBatis、Redis 等公共库。以前升级 Spring Boot 版本,得挨个改 20 个文件,改完还要检查有没有遗漏。用 Catalog 之后,只需要改 libs.versions.toml 里的一行。

来看一个多模块项目的典型结构:

my-project/
├── gradle/
│   └── libs.versions.toml      # 版本目录
├── settings.gradle.kts
├── build.gradle.kts            # 根项目(通常只做聚合)
├── common/
│   └── build.gradle.kts        # 公共模块
├── service-user/
│   └── build.gradle.kts        # 用户服务
├── service-order/
│   └── build.gradle.kts        # 订单服务
└── web-api/
    └── build.gradle.kts        # API 网关

每个子模块的 build.gradle.kts 里,依赖写法完全一致:

// common/build.gradle.kts
dependencies {
    implementation(libs.spring.boot.starter.web)
    implementation(libs.mybatis.plus.spring.boot3)
    implementation(libs.guava)
    implementation(libs.hutool.all)
    compileOnly(libs.lombok)
    annotationProcessor(libs.lombok)
}

// service-user/build.gradle.kts
dependencies {
    implementation(project(":common"))
    implementation(libs.spring.boot.starter.web)
    implementation(libs.mybatis.plus.spring.boot3)
    testImplementation(libs.bundles.testing)
}

你发现没有?所有模块引用的 Spring Boot 版本都来自同一个地方。哪天 Spring Boot 出了 3.2.1,我只需要在 toml 文件里把 spring-boot = "3.2.1",然后重新构建,所有模块自动升级。

多模块版本管理的三个最佳实践:

  1. 公共依赖放 bundles:比如所有模块都要用的 spring-boot-starter-web、lombok,可以打包成一个 bundle,子模块一行引入。
  2. 版本号命名要有规律:我习惯用「库名-版本」的格式,比如 spring-boot、mybatis-plus,一眼就能看出是什么。
  3. 不要混用 Catalog 和直接声明:要么全部用 Catalog,要么不用。混着用会让新人困惑,也失去了统一管理的意义。

说到 bundles,我再补充一句。如果你有多个模块都依赖同一组库,比如每个 Web 模块都要引入 Spring Boot Web、Jackson、Validation,你可以这样:

# libs.versions.toml
[bundles]
web-common = [
    "spring-boot-starter-web",
    "jackson-databind",
    "spring-boot-starter-validation"
]

# 子模块里
dependencies {
    implementation(libs.bundles.web.common)
}

这样既减少了重复代码,又保证了依赖的一致性。我现在的项目里,bundles 用得比 libraries 还多,因为实际开发中很少单独引入一个依赖,都是一组一组地引入。

一个小技巧:如果你在升级依赖时不确定会不会出问题,可以在 toml 文件里保留旧版本号,用注释标记。比如:

[versions]
# spring-boot = "3.1.6"  # 旧版本,回滚时取消注释
spring-boot = "3.2.0"    # 新版本

这样回滚时只需要交换注释,不用去翻 Git 历史。嗯,这招是我在一次紧急回滚后总结出来的。

最后说一句,Version Catalog 不是银弹。如果你的项目只有两三个模块,依赖也不多,用不用 Catalog 差别不大。但一旦模块超过 5 个,或者你经历过「升级依赖漏改一个模块导致线上事故」的痛,你就会爱上这个工具。


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