使用 动态依赖版本(例如,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 或变更依赖项,如果依赖项引入了错误或不兼容性,这可能导致意外故障。

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

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

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

  • 增强的开发工作流程:开发人员可以在处理功能或调试问题时在本地锁定依赖项以获得稳定性,而 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 版本通常包含在制品坐标中

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 或 snapshot 构建很有用。

锁定模式可以通过 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

通配符,用 * 表示,可用于 group 或 module 名称中。它们可以是唯一字符,也可以分别出现在 group 或 module 的末尾。以下通配符表示法示例有效

  • 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> 表示法指定依赖项,其中 * 作为尾部通配符。请注意,不接受 *:*,因为它实际上禁用了锁定。有关更多详细信息,请参阅关于更新锁定文件的描述

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

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

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

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

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

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

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

理解锁定限制

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