返回

Gradle插件版本管理:Version Catalog为何优于pluginManagement

java

Gradle 插件版本管理:为什么推荐版本目录 (Version Catalog) 而不是 pluginManagement

在使用 Gradle 管理项目依赖和插件时,你可能会遇到两种主要方式来控制插件版本:一种是 settings.gradle(.kts) 文件里的 pluginManagement { plugins { } } 块,另一种是现在越来越流行的版本目录(Version Catalog)。

不少人都清楚这两者的基本区别:

  • pluginManagement { plugins { } } 块(位于 settings.gradle):它的作用是配置 插件的版本和解析策略。它本身不应用 任何插件,只是为后面实际应用插件时提供版本信息。有点像个“插件版本声明中心”。
  • 顶层的 plugins { } 块(位于 settings.gradlebuild.gradle):这个块负责解析并实际应用 插件到对应的项目(根项目或子项目)。

问题来了,既然 pluginManagement 也能集中管理插件版本,为什么 Gradle 官方文档、Kotlin 官方文档以及许多社区讨论都更推荐使用版本目录呢?仅仅是为了集中管理和方便动态获取版本号吗?还是有更深层次的原因,比如构建性能、可维护性或者代码清晰度上的优势?

这篇文章就来聊聊,相比于 pluginManagement { plugins { } },版本目录在管理 Gradle 插件版本方面,到底好在哪儿。

回顾:pluginManagementplugins 的分工

咱们先快速过一下这两个配置块的职责,确保理解一致。

settings.gradle(.kts) 文件中的 pluginManagement

// settings.gradle.kts

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
    plugins {
        // 只声明版本,不应用
        id("com.android.application") version "8.2.0" apply false
        id("org.jetbrains.kotlin.android") version "1.9.20" apply false
        id("com.google.devtools.ksp") version "1.9.20-1.0.14" apply false
    }
}

// ... 其他 settings 配置

这里面的 plugins { ... } 块只是告诉 Gradle:“嘿,如果项目里要用 com.android.application 这个插件,记得用 8.2.0 版本,去 pluginManagement 里指定的仓库找。” apply false 明确表示这里不应用插件。

build.gradle(.kts) 文件(或 settings.gradle 顶层)中的 plugins

// app/build.gradle.kts

plugins {
    // 实际应用插件,版本由 settings.gradle 中的 pluginManagement 控制
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.devtools.ksp")
}

// ... build script 内容

在模块的 build.gradle.kts 文件里,plugins { ... } 块直接写插件 ID。Gradle 看到这些 ID,就会去 settings.gradlepluginManagement 块里查找对应的版本信息,然后下载并应用插件。

简单说,pluginManagement 管“版本规定”,顶层 plugins 管“实际执行”。

pluginManagement.plugins {} 的局限性

pluginManagement 确实解决了在每个 build.gradle 文件里硬编码插件版本导致的版本冲突和管理混乱问题。它提供了一个集中的地方来声明版本。但这还不够完美,尤其是在复杂项目或需要更高维护性的场景下,它存在一些不足:

  1. 仅限于插件管理: pluginManagement 顾名思义,主要处理插件。但项目里还有大量的库依赖(dependencies)版本需要管理。如果插件版本在这里管,库依赖版本在 buildSrcext 属性里管,那版本管理就分散了,不够统一。
  2. 缺乏类型安全和 IDE 支持:plugins {} 块里引用插件时,我们通常使用字符串 ID,比如 id("com.android.application")。这容易打错字,而且 IDE 的自动补全、重构支持也相对有限。如果插件 ID 改了,或者你想找某个插件在哪些地方被用了,字符串搜索有时不够精确。
  3. 共享和重用不方便: pluginManagement 配置是写在 settings.gradle 里的。如果想在多个独立的项目之间共享一套标准的插件版本配置,直接复制粘贴 settings.gradle 的部分内容显然不太优雅,也容易出错。想把它抽成一个独立的、可发布、可版本化的单元比较困难。
  4. 可读性可以更好: 当插件列表很长时,pluginManagement 块也会变得臃肿。虽然比分散在各处好,但不如专门的配置文件(比如 TOML)来得清晰。

正是因为这些局限性,Gradle 引入了功能更强大的版本目录(Version Catalog)。

版本目录 (Version Catalog):更优的方案

版本目录提供了一种更结构化、更灵活、更强大的方式来集中管理所有类型的依赖项(包括库和插件)及其版本。它通常定义在一个名为 libs.versions.toml 的文件中,放在 gradle/ 目录下。

版本目录的核心优势

对比 pluginManagement.plugins {},版本目录在管理插件版本(以及依赖版本)方面具有显著优势:

1. 统一的版本管理中心

这是最直观的好处。版本目录不光能管插件版本,还能管理库依赖的版本。所有版本信息都集中在 libs.versions.toml 这一个地方。

# gradle/libs.versions.toml

[versions]
androidGradlePlugin = "8.2.0"
kotlin = "1.9.20"
ksp = "1.9.20-1.0.14"
appcompat = "1.6.1"
coreKtx = "1.12.0"

[libraries]
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }

[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

# 可以定义 bundle 方便一次性应用或依赖
[bundles]
androidx-ui = ["androidx-appcompat", "androidx-core-ktx"]

看,[versions][libraries][plugins][bundles] 各司其职,清晰明了。库依赖和插件版本用同一套 [versions] 定义,维护起来更方便。再也不用纠结某个版本定义应该放 pluginManagement 还是其他地方了。

2. 类型安全与 IDE 智能提示

当你定义了版本目录(默认是 libs.versions.toml)后,Gradle 会自动为你生成类型安全的访问器(accessors)。在 build.gradle.kts 文件里,你可以像访问对象属性一样引用插件和库。

以前 (使用 pluginManagement):

// app/build.gradle.kts
plugins {
    id("com.android.application") // 字符串,容易 typo
    id("org.jetbrains.kotlin.android")
}

dependencies {
    implementation("androidx.core:core-ktx:1.12.0") // 字符串,版本硬编码或来自 ext
}

现在 (使用版本目录):

// app/build.gradle.kts
plugins {
    alias(libs.plugins.android.application) // 类型安全,IDE 自动补全
    alias(libs.plugins.kotlin.android)      // 重构友好
}

dependencies {
    implementation(libs.androidx.core.ktx)  // 类型安全,版本来自 TOML
    implementation(libs.bundles.androidx.ui) // 使用 bundle
}

注意,这里用了 alias() 包裹插件引用。这是因为在 plugins {} 块中,Gradle 期望的是插件 ID 和版本(如果版本没在 pluginManagement 或版本目录里声明的话)。版本目录生成的访问器 libs.plugins.android.application 本身是一个 PluginDependency 对象。通过 alias(),我们告诉 Gradle:“去版本目录里找这个别名对应的插件 ID 和版本信息”。

这种类型安全的方式极大地减少了因手误导致的构建错误,IDE 也能提供更好的自动补全、跳转到定义、查找用例等支持,代码也更易读。

3. 易于共享和重用

libs.versions.toml 是一个独立的文件。你可以:

  • 把它放在项目的 gradle/ 目录下,供项目内所有模块使用。
  • 把它放在一个独立的 Git 仓库中,通过 Git Submodule 或直接复制到多个项目中,实现跨项目的版本标准化。
  • 更高级的用法: 将版本目录本身发布为一个 Maven 构件(例如,使用 java-gradle-plugin 或特定插件)。其他项目可以通过 settings.gradle 中的 dependencyResolutionManagement 来依赖这个发布的目录,实现大规模、可靠的版本共享。

相比之下,共享 pluginManagement 配置就要麻烦得多,通常只能通过复制粘贴或自定义的 Gradle 脚本逻辑实现,缺乏标准化的机制。

4. 提升可读性与可维护性

TOML 文件格式本身就非常简洁易读。版本目录的结构([versions], [libraries], [plugins], [bundles])使得版本信息组织有序。

  • 别名 (Alias): 你可以为插件和库设置有意义的别名(如 libs.plugins.android.application 中的 android.application),而不是直接使用冗长的 GAV (Group:Artifact:Version) 坐标或插件 ID。
  • 版本引用 (version.ref): 避免在多个地方重复写版本号,改动版本时只需修改 [versions] 块中的一处。
  • 捆绑 (Bundles): 可以将一组相关的库或插件定义为一个 bundle,在 dependenciesplugins 块中一次性引用,使构建脚本更简洁。

这些特性共同提升了整个项目构建配置的可读性和长期可维护性。

5. 与 Gradle 生态更紧密集成

版本目录是 Gradle 官方推荐的依赖管理方式,与 Gradle 的其他特性(如依赖项约束、平台 platform)集成得更好。它代表了 Gradle 依赖管理发展的方向。

6. 官方推荐与社区趋势

就像问题中提到的,Gradle 官方文档和 Kotlin Gradle 插件的最佳实践都明确推荐使用版本目录。这意味着 Gradle 团队会持续投入资源改进版本目录的功能和体验。遵循官方推荐通常能让你获得更好的支持和更平滑的 Gradle 版本升级体验。社区也普遍接受并转向使用版本目录。

性能考量?

至于版本目录是否比 pluginManagement 带来显著的构建性能提升,目前没有明确的证据表明在插件版本解析方面有本质区别。两者最终都是在配置阶段确定插件的版本。性能优势可能更多体现在:

  • 统一管理带来的依赖解析优化(如果库依赖也纳入管理)。
  • 类型安全访问器可能在 Gradle 内部处理时有微小优势(避免字符串解析等)。

但性能不应是选择版本目录的主要驱动因素。维护性、可读性、类型安全、易共享性 才是它压倒性的优势所在。

如何使用版本目录管理插件?

实际操作起来很简单:

步骤 1: 创建 gradle/libs.versions.toml 文件

如果项目还没有这个文件,手动创建它,并添加基本的结构:

# gradle/libs.versions.toml

[versions]
# 定义版本变量

[libraries]
# 定义库依赖

[plugins]
# 定义插件!!!

[bundles]
# 定义捆绑包

步骤 2: 在 [plugins] 部分定义你的插件

使用一个有意义的别名(如 androidApplicationkotlin-jvm),提供插件的 idversion (或 version.ref)。

# gradle/libs.versions.toml

[versions]
agp = "8.2.0"
kotlin = "1.9.20"
shadow = "8.1.1"

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
# 如果插件发布在自定义仓库,且也托管了版本目录,可能需要不同的声明方式
# 对于纯粹定义ID和版本给项目使用的,上面的格式是标准的

# 例子:一个不需要版本引用的插件 (版本在Gradle插件门户已经定义)
# 或者你想在 settings.gradle 中 apply 但不关心特定版本管理(不推荐)
# shadow-jar = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } # 完整定义

# 更推荐的格式,明确 ID 和版本来源
shadowJar = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }

步骤 3: 在 settings.gradle(.kts) 中启用版本目录

通常不需要显式启用,Gradle 默认会查找 gradle/libs.versions.toml。但如果你用了非默认的文件名或想配置,可以在 dependencyResolutionManagement 中设置:

// settings.gradle.kts

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        // 如果插件来自特定仓库,这里也要加上
        gradlePluginPortal() // 通常需要这个来找插件本身
    }
    versionCatalogs {
        create("libs") { // "libs" 是默认目录名
            from(files("../gradle/libs.versions.toml")) // 指定文件路径,通常自动检测可省略
        }
    }
}

// 在 settings.gradle 中直接应用插件(如果需要,如 Android 的 application/library 插件)
// 注意:在 settings.gradle 的顶层 plugins 块应用插件时,仍需 pluginManagement 来提供版本
// 这似乎与版本目录的目标有点冲突,但这是目前 Android Gradle Plugin 的要求
// 推荐的方式是,只在 project 的 build.gradle 中应用插件,并通过版本目录引用

// -------- 如果必须在 settings.gradle 应用插件 --------
// 你仍然可以在 pluginManagement 中保留这个插件的版本声明,
// 让顶层 plugins { } 块可以找到它。
// 或者,更好的方式是避免在 settings.gradle 中应用插件,除非绝对必要。
pluginManagement {
     // ... repositories ...
     plugins {
         // 这里只是为了让 settings.gradle 的顶层 plugins 能找到版本
         // 实际版本来源还是 TOML 定义的
         id("com.android.application").versionRef("agp").apply(false)
     }
 }

 plugins {
     // 这个 id 会使用 pluginManagement 提供的版本(该版本来自 versionRef 指向 TOML)
     id("com.android.application")
 }

 //...

关于在 settings.gradle 中应用插件的说明:这是一个稍微复杂的情况。根据 Gradle 文档和当前实践,settings.gradle 顶层的 plugins {} 块在解析插件时,优先查找 pluginManagement {} 中的声明。如果你的插件(比如 com.android.application必须settings.gradle 中应用,那么即使你用版本目录定义了它的版本,可能仍需要在 pluginManagement {} 里添加一个指向版本目录中版本的引用 (如 .versionRef("agp")),并设置 apply(false)。这样 settings.gradleplugins {} 块才能成功解析。这感觉有点冗余,是当前 Gradle 对设置插件处理的一个特点。

最佳实践是: 尽量只在各个项目(模块)的 build.gradle(.kts) 文件中使用 plugins {} 应用插件。这样可以直接、清晰地使用版本目录提供的别名。

步骤 4: 在 build.gradle(.kts) 文件中应用插件

这是最常见和推荐的方式。使用版本目录生成的类型安全访问器,通过 alias() 方法。

// app/build.gradle.kts

plugins {
    alias(libs.plugins.android.application) // 清晰、类型安全
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.shadowJar)           // 假设也定义了 shadowJar 插件
}

android {
    // ...
}

dependencies {
    implementation(libs.androidx.core.ktx)
    // ...
}

进阶技巧

  • 插件捆绑 (Plugin Bundles): 虽然 TOML 里可以定义 [bundles] 主要用于库,但你也可以在 plugins {} 块中逻辑上组合应用,例如通过封装 convention plugins 实现。直接在 TOML 定义插件 bundle 目前支持有限,通常通过别名逐个引用。
  • 共享版本目录: 前面提到,可以通过独立 Git 仓库或发布构件的方式共享 libs.versions.toml。发布构件是大型组织或开源项目常用的方式,可以确保所有消费者使用统一、测试过的版本集合。

总结一下

对比 pluginManagement { plugins {} },使用版本目录(Version Catalog)管理 Gradle 插件版本之所以成为最佳实践,主要因为它提供了:

  1. 统一管理 :集中管理库依赖和插件的版本,减少配置分散。
  2. 类型安全 :通过生成的访问器,减少拼写错误,提升 IDE 支持。
  3. 易于共享 :独立的 TOML 文件更容易在项目间共享和版本化。
  4. 可读性与维护性 :清晰的结构、别名、版本引用和捆绑,让配置更易懂、易维护。
  5. 与 Gradle 生态集成更好 :符合 Gradle 发展方向,与新特性配合更佳。
  6. 官方和社区推荐 :获得更好的支持和更平滑的升级路径。

虽然 pluginManagement 解决了最初的痛点,但版本目录提供了一个更全面、更健壮、更面向未来的解决方案。如果你还在犹豫是否迁移,考虑到这些实实在在的好处,开始尝试使用版本目录吧,它会让你的 Gradle 构建配置更加清晰和专业。