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

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

为确保可复现构建,有必要锁定依赖项及其传递依赖项的版本。这保证了具有相同输入的构建总是解析到相同的模块版本,这一过程称为依赖锁定

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

依赖锁定提供了几个关键优势

  • 避免级联失败:管理多个仓库的团队不再需要依赖 -SNAPSHOT 或经常变化的依赖,这可能导致如果依赖引入了 bug 或不兼容性而导致意外失败。

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

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

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

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

激活特定配置的锁定

锁定是针对每个依赖配置启用的。

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

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

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

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

配置的锁定通过 ResolutionStrategy API 实现

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

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

激活所有配置的锁定

以下将锁定所有配置

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

上述操作将锁定所有项目配置,但不包括构建脚本配置。

禁用特定配置的锁定

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

如果一个插件配置了所有配置的锁定,但您碰巧添加了一个不应该被锁定的配置,这可能会很有用。

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

激活构建脚本 classpath 配置的锁定

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

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

build.gradle.kts
buildscript {
    configurations.classpath {
        resolutionStrategy.activateDependencyLocking()
    }
}
build.gradle
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. 声明一个自定义任务来解析所有配置

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

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

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() }
    }
}
build.gradle
tasks.register('resolveAndLockAll') {
    notCompatibleWithConfigurationCache("Filters configurations at execution time")
    doFirst {
        assert gradle.startParameter.writeDependencyLocks : "$path must be run from the command line with the `--write-locks` flag"
    }
    doLast {
        configurations.findAll {
            // Add any custom filtering on the configurations to be resolved
            it.canBeResolved
        }.each { it.resolve() }
    }
}

通过过滤和解析特定配置,您可以确保只有相关的配置被锁定,从而根据项目需求定制锁定过程。这在像原生构建这样的环境中特别有用,因为并非所有配置都可以在单个平台上解析。

理解锁定状态的位置和格式

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

锁定文件应提交到源代码管理。

锁定文件的位置

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

  • 唯一的例外是构建脚本本身的锁定文件,其名称为 buildscript-gradle.lockfile

锁定文件的结构

考虑以下依赖声明

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

dependencies {
    implementation("org.springframework:spring-beans:[5.0,6.0)")
}
build.gradle
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 版本通常包含在 Artifact 坐标中

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

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

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

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

完整的验证如下

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

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

  • 解析结果不能包含比锁定状态更多的额外依赖

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

虽然默认锁定模式如上所述,但还有另外两种模式可用

严格模式

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

宽松模式

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

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

  • 允许传递依赖项发生变化,只要动态版本仍然固定。

此模式为需要探索或测试新依赖项或版本更改而不破坏构建的情况提供了灵活性,使其在测试 nightly 或快照构建时非常有用。

锁定模式可以通过 dependencyLocking 块控制,如下所示

build.gradle.kts
dependencyLocking {
    lockMode = LockMode.STRICT
}
build.gradle
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 项目扩展中配置要忽略的依赖项

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

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

忽略依赖项将产生以下影响

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

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

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

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

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

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

理解锁定限制

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