19、版本目录:libs.versions.toml、版本对齐、目录插件

版本管理,说白了就是项目里的「依赖大管家」。我见过太多项目,几十个模块各自定义版本号,升级一个库要改十几个文件,改完还经常漏掉。嗯,Gradle 7.0 引入的版本目录(Version Catalog)就是为了解决这个痛点。

19.1 为什么需要版本目录?

先说说我踩过的坑。曾经有个多模块项目,Spring Boot 版本分散在 6 个 build.gradle.kts 里。升级 2.6 到 2.7 时,我手动改了 5 个文件,结果漏掉了一个模块的测试依赖。编译没问题,运行时直接 ClassNotFoundException。你想想看,这种低级错误多耽误事。

版本目录的核心价值就三点:

  • 统一管理:所有版本号集中在一个文件里
  • 类型安全:Kotlin DSL 里直接调用,编译期就能发现拼写错误
  • 跨项目共享:可以发布到仓库,多个项目共用一套版本约束

核心思想:把版本号从构建脚本中剥离出来,让 build.gradle.kts 只关注「用什么」,不关心「用哪个版本」。

19.2 libs.versions.toml 文件结构

这个文件通常放在 gradle/ 目录下。我个人习惯叫它「版本清单」,因为它就像超市购物清单一样,把需要的东西列清楚就行。

# gradle/libs.versions.toml

[versions]
kotlin = "1.9.0"
coroutines = "1.7.3"
retrofit = "2.9.0"
okhttp = "4.11.0"

[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }

[bundles]
networking = ["retrofit-core", "retrofit-gson", "okhttp-core"]

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

这个文件分四个区域:

区域 作用 示例
[versions] 定义版本号常量 kotlin = "1.9.0"
[libraries] 声明依赖坐标 retrofit-core = { module = "...", version.ref = "retrofit" }
[bundles] 将多个依赖打包 networking = ["retrofit-core", "okhttp-core"]
[plugins] 声明插件版本 kotlin-jvm = { id = "...", version.ref = "kotlin" }

小技巧:version.ref 引用的是 [versions] 里的键名。如果版本号直接写在 library 里,用 version = "1.0.0" 也行,但不推荐——失去了统一管理的意义。

19.3 在 build.gradle.kts 中使用版本目录

配置好 toml 文件后,在构建脚本里就能直接调用了。注意看,这里没有字符串,全是类型安全的访问器:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
    versionCatalogs {
        create("libs") {
            from(files("gradle/libs.versions.toml"))
        }
    }
}

// build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.kotlin.serialization)
}

dependencies {
    implementation(libs.kotlin.stdlib)
    implementation(libs.bundles.networking)
    
    // 也可以单独引用 bundle 里的某个库
    testImplementation(libs.kotlinx.coroutines.core)
}

为什么说类型安全?因为 libs.kotlin.stdlib 如果拼写错误,IDE 直接标红,编译直接报错。我曾经在传统方式里把 kotlin-stdlib 写成 kotlin-stdlib-jdk8,编译过了,但运行时找不到类。用版本目录后,这种问题再也没出现过。

19.4 版本对齐:解决依赖冲突

版本对齐,说白了就是「让所有模块用同一个版本的库」。举个例子:模块 A 依赖 OkHttp 4.10,模块 B 依赖 OkHttp 4.11,最终打包时到底用哪个?

Gradle 默认用最高版本,但这不一定安全。我遇到过 OkHttp 4.10 和 4.11 之间有个 API 被标记为 deprecated,结果运行时日志刷屏警告。

版本目录配合 平台依赖(Platform) 可以强制对齐:

// 在 toml 里定义平台
[libraries]
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version = "4.11.0" }
okhttp-core = { module = "com.squareup.okhttp3:okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor" }

// 在 build.gradle.kts 里导入 BOM
dependencies {
    implementation(platform(libs.okhttp.bom))
    implementation(libs.okhttp.core)
    implementation(libs.okhttp.logging)
}

这样所有 OkHttp 相关依赖都强制使用 4.11.0 版本,不会出现版本漂移。

注意:版本对齐不是万能的。如果两个库依赖了同一个库的不同不兼容版本(比如 Guava 27 vs 30),BOM 也救不了你。这时候需要看是不是有传递依赖冲突,或者考虑用 Gradle 的强制版本(force)来解决。

19.5 目录插件:自动生成访问器

Gradle 7.0 之后,版本目录默认会生成类型安全的访问器。但如果你用的是 Gradle 6.x,或者想自定义访问器名称,可以用 目录插件(Version Catalog Plugin)

我个人建议:只要项目 Gradle 版本 >= 7.0,直接用内置功能就行。目录插件更多是为了兼容旧版本或者做高级定制。

// 如果需要自定义,在 settings.gradle.kts 里配置
dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            from(files("gradle/libs.versions.toml"))
            // 自定义访问器名称
            library("my-okhttp", "com.squareup.okhttp3", "okhttp").version("4.11.0")
        }
    }
}

访问器命名规则:

  • 点号(.)和短横线(-)会被替换成点号
  • 比如 kotlin-stdlib 变成 libs.kotlin.stdlib
  • kotlinx-coroutines-core 变成 libs.kotlinx.coroutines.core

避坑指南:我曾经把 library 的 key 写成 kotlin-stdlib-jdk8,结果访问器变成了 libs.kotlin.stdlib.jdk8。虽然能用,但看着别扭。建议 key 的命名保持简洁,不要带版本后缀。

19.6 多项目共享版本目录

如果你在维护多个项目,可以把版本目录发布到仓库,让所有项目引用同一个版本清单。这样升级一个库时,改一个文件就能同步所有项目。

// 发布版本目录(在独立的发布项目中)
plugins {
    `version-catalog`
    `maven-publish`
}

catalog {
    versionCatalog {
        from(files("gradle/libs.versions.toml"))
    }
}

publishing {
    publications {
        create("catalog") {
            from(components["versionCatalog"])
        }
    }
}

// 其他项目引用
dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            from("com.example:catalog:1.0.0")
        }
    }
}

这种方式特别适合微服务架构。我参与过一个 20+ 模块的项目,每个模块独立发布,但版本目录统一管理。升级 Spring Boot 时,只需要改一个 toml 文件,重新发布目录,所有模块自动同步。

19.7 本章知识体系

下面这张图展示了版本目录的核心逻辑和流转关系:

版本目录核心架构 libs.versions.toml 版本清单(统一入口) 手动编写 开发者维护版本号 远程仓库发布 跨项目共享目录 build.gradle.kts 类型安全访问器 settings.gradle.kts 目录配置入口 版本对齐 BOM + 平台依赖 目录插件 自定义访问器 跨项目共享 发布到 Maven 仓库

从这张图可以看出,版本目录是整个依赖管理的枢纽。它从 toml 文件或远程仓库获取版本信息,然后通过类型安全的访问器注入到构建脚本中。版本对齐、目录插件、跨项目共享都是基于这个核心机制扩展出来的能力。

总结一下:版本目录不是银弹,但它解决了 80% 的版本管理痛点。我建议所有新项目都从第一天开始用,老项目也值得花半天时间迁移。相信我,当你下次升级依赖时,你会感谢这个决定的。


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