使用动态依赖版本 (例如,1.+[1.0,2.0)) 可能会导致构建意外中断,因为解析的依赖项的确切版本会随时间而变化

KotlinGroovy
build.gradle.kts
dependencies {
    // Depend on the latest 5.x release of Spring available in the searched repositories
    implementation("org.springframework:spring-web:5.+")
}

为确保 可重现的构建,必须锁定版本的依赖项及其传递依赖项。这保证了具有相同输入的构建将始终解析为相同的模块版本,此过程称为依赖锁定

依赖锁定是一个过程,Gradle 将已解析的依赖项版本保存到锁定文件中,从而确保后续构建使用相同的依赖项版本。此锁定状态存储在一个文件中,有助于防止依赖关系图中的意外更改。

依赖锁定具有以下几个主要优点

  • 避免级联失败:管理多个存储库的团队不再需要依赖 -SNAPSHOT 或更改依赖项,如果依赖项引入了错误或不兼容性,这可能会导致意外失败。

  • 动态版本灵活性与稳定性:使用最新版本依赖项的团队可以在开发和测试阶段依赖动态版本,仅在发布时锁定它们。

  • 发布已解析的版本:通过将依赖锁定与发布已解析的版本的实践相结合,动态版本将在发布时替换为实际解析的版本。

  • 优化构建缓存使用:由于动态或更改的依赖项违反了稳定的任务输入原则,因此锁定依赖项可确保任务具有一致的输入。

  • 增强的开发工作流程:开发人员可以在本地锁定依赖项以在处理功能或调试问题时保持稳定性,而 CI 环境可以测试最新的 SNAPSHOT 或 nightly 版本,以提供有关集成问题的早期反馈。这使团队可以在开发过程中平衡稳定性和早期反馈。

为特定配置启用锁定

锁定是按依赖配置启用的。

启用后,您必须创建初始锁定状态,使 Gradle 验证解析结果是否未更改。这确保了如果选择的依赖项与锁定的依赖项不同(由于有较新版本可用),构建将失败,从而防止意外的版本更改。

依赖锁定对动态版本有效,但不更改的版本(例如,-SNAPSHOT)一起使用,在更改的版本中,坐标保持不变,但内容可能会更改。

将依赖锁定与更改的版本一起使用表明对这些功能的误解,并可能导致不可预测的结果。

如果解析结果中存在更改的依赖项,则在持久化锁定状态时,Gradle 将发出警告。

配置的锁定通过 ResolutionStrategy API 进行

KotlinGroovy
build.gradle.kts
configurations {
    compileClasspath {
        resolutionStrategy.activateDependencyLocking()
    }
}

只有可以解析的配置才会附加锁定状态。在不可解析的配置上应用锁定是无效操作。

为所有配置启用锁定

以下代码将锁定所有配置

KotlinGroovy
build.gradle.kts
dependencyLocking {
    lockAllConfigurations()
}

以上代码将锁定所有项目配置,但不锁定 buildscript 配置。

为特定配置禁用锁定

您还可以禁用特定配置的锁定。

如果插件配置了所有配置的锁定,但您碰巧添加了一个不应锁定的配置,这将非常有用

KotlinGroovy
build.gradle.kts
configurations.compileClasspath {
    resolutionStrategy.deactivateDependencyLocking()
}

为 buildscript classpath 配置启用锁定

如果将插件应用于构建,您可能也希望在那里利用依赖锁定。

要锁定用于脚本插件的classpath 配置

KotlinGroovy
build.gradle.kts
buildscript {
    configurations.classpath {
        resolutionStrategy.activateDependencyLocking()
    }
}

生成和更新依赖锁定

要生成或更新锁定状态,请在调用任何将触发锁定配置解析的任务时添加 --write-locks 参数

$ ./gradlew dependencies --write-locks

这将为该构建执行期间解析的每个配置创建或更新锁定状态。如果锁定状态已存在,它将被覆盖。

gradle.lockfile
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.google.code.findbugs:jsr305:3.0.2=classpath
com.google.errorprone:error_prone_annotations:2.3.2=classpath
com.google.gradle:osdetector-gradle-plugin:1.7.1=classpath
com.google.guava:failureaccess:1.0.1=classpath
com.google.guava:guava:28.1-jre=classpath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath
com.google.j2objc:j2objc-annotations:1.3=classpath
empty=
如果构建失败,Gradle 将不会将锁定状态写入磁盘,从而防止持久化潜在的无效状态。

在单次构建执行中锁定所有配置

当处理多个配置时,您可能希望在单次构建执行中一次性锁定它们。您有两种选择

  1. 运行 gradle dependencies --write-locks

    • 此命令将锁定所有已启用锁定的可解析配置。

    • 在多项目设置中,请注意 dependencies 仅在一个项目(通常是根项目)上执行。

  2. 声明自定义任务以解析所有配置

    • 如果您需要更多地控制锁定哪些配置,则此方法特别有用。

此自定义任务解析所有配置,并在过程中锁定它们

KotlinGroovy
build.gradle.kts
tasks.register("resolveAndLockAll") {
    notCompatibleWithConfigurationCache("Filters configurations at execution time")
    doFirst {
        require(gradle.startParameter.isWriteDependencyLocks) { "$path must be run from the command line with the `--write-locks` flag" }
    }
    doLast {
        configurations.filter {
            // Add any custom filtering on the configurations to be resolved
            it.isCanBeResolved
        }.forEach { it.resolve() }
    }
}

通过过滤和解析特定配置,您可以确保仅锁定相关的配置,从而根据项目的需求定制锁定过程。这在诸如本机构建之类的环境中尤其有用,在这些环境中,并非所有配置都可以在单个平台上解析。

理解锁定状态位置和格式

锁定文件是一个关键组件,它记录项目中使用的依赖项的确切版本,从而可以在构建期间进行验证,以确保跨不同环境和随时间推移结果的一致性。当项目在不同的机器上或在不同的时间构建时,它有助于识别依赖项中的差异。

锁定文件应检入源代码控制。

锁定文件的位置

  • 锁定状态保存在名为 gradle.lockfile 的文件中,该文件位于每个项目或子项目目录的根目录中。

  • 例外情况是 buildscript 本身的锁定文件,该文件名为 buildscript-gradle.lockfile

锁定文件的结构

考虑以下依赖项声明

KotlinGroovy
build.gradle.kts
configurations {
    compileClasspath {
        resolutionStrategy.activateDependencyLocking()
    }
    runtimeClasspath {
        resolutionStrategy.activateDependencyLocking()
    }
    annotationProcessor {
        resolutionStrategy.activateDependencyLocking()
    }
}

dependencies {
    implementation("org.springframework:spring-beans:[5.0,6.0)")
}

使用以上配置,生成的 gradle.lockfile 将如下所示

gradle.lockfile
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
org.springframework:spring-beans:5.0.5.RELEASE=compileClasspath, runtimeClasspath
org.springframework:spring-core:5.0.5.RELEASE=compileClasspath, runtimeClasspath
org.springframework:spring-jcl:5.0.5.RELEASE=compileClasspath, runtimeClasspath
empty=annotationProcessor

其中

  • 每一行代表 group:artifact:version 格式的单个依赖项。

  • 配置: 在版本之后,列出了包含依赖项的配置。

  • 排序: 依赖项和配置按字母顺序列出,以使版本控制差异更易于管理。

  • 空配置: 最后一行列出了空配置,这意味着它们不包含任何依赖项。

锁定文件应包含在源代码控制中,以确保所有团队成员和环境都使用完全相同的依赖项版本。

迁移您的旧版锁定文件

如果您的项目使用每个锁定配置文件的旧版锁定文件格式,请按照以下说明迁移到新格式

  1. 请按照关于写入更新依赖锁定状态的文档进行操作。

  2. 在写入每个项目的单个锁定文件后,Gradle 还会删除状态已转移的所有配置的每个配置文件锁定文件。

迁移可以一次完成一个配置。只要单个锁定文件中没有该配置的信息,Gradle 将继续从每个配置的文件中获取锁定状态。

配置锁定文件名和位置

当使用每个项目的单个锁定文件时,您可以配置其名称和位置。

此功能允许您指定基于项目属性的文件名,从而使单个项目可以为不同的执行上下文存储不同的锁定状态。

例如,在 JVM 生态系统中,Scala 版本通常包含在工件坐标中

KotlinGroovy
build.gradle.kts
val scalaVersion = "2.12"
dependencyLocking {
    lockFile = file("$projectDir/locking/gradle-${scalaVersion}.lockfile")
}

在存在锁定状态的情况下运行构建

当构建需要解析已启用锁定的配置时,并且它找到匹配的锁定状态,它将使用它来验证给定配置是否仍解析相同的版本。

成功的构建表明您的构建使用的依赖项与锁定状态中存储的依赖项相同,无论是否有与构建使用的任何仓库中的动态选择器匹配的新版本可用。

完整的验证如下

  • 锁定状态中的现有条目必须在构建中匹配

    • 版本不匹配或缺少已解析的模块会导致构建失败

  • 与锁定状态相比,解析结果不得包含额外的依赖项

使用锁定模式微调依赖锁定行为

虽然默认锁定模式的行为如上所述,但还有两种其他模式可用

严格模式

在此模式下,除了上述验证之外,如果标记为锁定的配置没有与之关联的锁定状态,则依赖锁定将失败。

宽松模式

在此模式下,依赖锁定仍将固定动态版本,但对依赖项解析的其他更改不再是错误。其他更改包括

  • 添加或删除依赖项,即使它们是严格版本控制的,也不会导致构建失败。

  • 允许传递依赖项转移,只要动态版本仍被固定。

此模式为可能想要探索或测试新依赖项或版本更改而不破坏构建的情况提供了灵活性,使其对于测试 nightly 或 snapshot 构建非常有用。

锁定模式可以从 dependencyLocking 代码块中控制,如下所示

KotlinGroovy
build.gradle.kts
dependencyLocking {
    lockMode = LockMode.STRICT
}

选择性地更新锁定状态条目

为了仅更新配置的特定模块,您可以使用 --update-locks 命令行标志。它接受以逗号 (,) 分隔的模块符号列表。在此模式下,现有锁定状态仍用作解析的输入,过滤掉更新目标模块

$ ./gradlew dependencies --update-locks org.apache.commons:commons-lang3,org.slf4j:slf4j-api

通配符(用 * 表示)可以在组名或模块名中使用。它们可以是唯一字符,也可以分别出现在组或模块的末尾。以下通配符表示法示例是有效的

  • org.apache.commons:*:将允许更新属于组 org.apache.commons 的所有模块

  • *:guava:将允许更新所有名为 guava 的模块,无论其组是什么

  • org.springframework.spring*:spring*:将允许更新所有组名以 org.springframework.spring 开头且名称以 spring 开头的模块

解析可能会导致其他模块版本更新,这由 Gradle 解析规则决定。

禁用依赖锁定

要禁用配置的依赖锁定

  1. 删除锁定配置: 确保您不再要锁定的配置未配置依赖锁定。这意味着删除或注释掉该配置的任何 activateDependencyLocking() 调用。

  2. 更新锁定状态: 下次更新并保存锁定状态(使用 --write-locks 选项)时,Gradle 将自动清理与不再锁定的配置关联的任何陈旧的锁定状态。

Gradle 必须解析不再标记为锁定的配置,以检测和删除关联的锁定状态。如果不解析配置,Gradle 无法识别应清理哪个锁定状态。

从锁定状态中忽略特定依赖

在某些情况下,您可能希望出于构建可重现性以外的其他原因使用依赖锁定。

作为构建作者,您可能希望某些依赖项比其他依赖项更频繁地更新。例如,组织内部的内部依赖项可能始终使用最新版本,而第三方依赖项则遵循不同的更新周期。

这种方法可能会损害可重现性。考虑为特定情况使用不同的锁定模式单独的锁定文件

您可以在 dependencyLocking 项目扩展中配置要忽略的依赖项

KotlinGroovy
build.gradle.kts
dependencyLocking {
    ignoredDependencies.add("com.example:*")
}

符号 <group>:<name> 用于指定依赖项,其中 * 充当尾随通配符。请注意,不接受 *:*,因为它实际上禁用了锁定。有关更多详细信息,请参阅关于更新锁定文件的描述

忽略依赖项将具有以下影响

  • 忽略的依赖项适用于所有锁定的配置,并且该设置是项目范围的。

  • 忽略依赖项不会将其传递依赖项从锁定状态中排除。

  • 没有验证确保忽略的依赖项存在于任何配置解析中。

  • 如果依赖项存在于锁定状态中,则加载它将过滤掉该依赖项。

  • 如果依赖项存在于解析结果中,则在根据锁定状态验证解析时,将忽略该依赖项。

  • 当锁定状态更新并持久化时,任何被忽略的依赖项都将从写入的锁定状态中省略。

理解锁定限制

  • 依赖锁定目前不适用于源代码依赖项。