使用动态依赖项版本(例如 1.+[1.0,2.0))会使构建变为非确定性。这会导致构建在没有任何明显更改的情况下中断,更糟糕的是,它可能由构建作者无法控制的传递依赖项引起。

为了实现可重复构建,有必要锁定依赖项和传递依赖项的版本,以便具有相同输入的构建始终解析相同的模块版本。这称为依赖项锁定

它支持以下场景,其中包括

  • 处理多个存储库的公司不再需要依赖 -SNAPSHOT 或更改依赖项,这有时会导致级联故障,因为依赖项会引入错误或不兼容性。现在,可以针对主要版本或次要版本范围声明依赖项,从而能够在 CI 上使用最新版本进行测试,同时利用锁定功能进行稳定的开发者构建。

  • 始终希望使用最新依赖项的团队可以使用动态版本,仅为发布锁定其依赖项。发布标签将包含锁定状态,从而允许在需要开发错误修复时完全可重复该构建。

结合发布已解析版本,您还可以在发布时替换已声明的动态版本部分。消费者将看到您的版本解析的结果版本。

锁定是针对每个依赖项配置启用的。启用后,您必须创建一个初始锁定状态。这将导致 Gradle 验证解析结果不会改变,即使生成了较新版本,也会导致选择相同的依赖项。对构建的修改会影响已解析的依赖项集,这将导致构建失败。这可确保已发布依赖项或构建定义中的更改不会在不调整锁定状态的情况下改变解析。

依赖项锁定仅对动态版本有意义。它不会对更改版本(例如 -SNAPSHOT)产生影响,其坐标保持不变,但内容可能会改变。当持久化锁定状态且解析结果中存在更改的依赖项时,Gradle 甚至会发出警告。

在配置中启用锁定

配置的锁定通过ResolutionStrategy进行

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 配置,请执行以下操作

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

生成和更新依赖项锁定

为了生成或更新锁定状态,除了会触发配置解析的常规任务之外,您还需要指定 --write-locks 命令行参数。这将为该构建执行中每个已解析的配置创建锁定状态。请注意,如果锁定状态先前存在,它将被覆盖。

如果构建失败,Gradle 不会将锁定状态写入磁盘。这可以防止保留可能无效的状态。

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

锁定多个配置时,您可能希望在一次构建执行期间一次性锁定所有配置。

为此,您有两个选择

  • 运行 gradle dependencies --write-locks。这将有效地锁定所有已启用锁定的可解析配置。请注意,在多项目设置中,dependencies 仅在一个项目(在本例中为根项目)上执行。

  • 声明一个解析所有配置的自定义任务。这对于 Android 项目不起作用。

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

锁定文件将具有以下内容

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 表示法中的单个依赖项

  • 然后列出包含给定依赖项的所有配置

  • 模块和配置按字母顺序排列,以简化差异

  • 文件最后一行列出所有空配置,即已知没有依赖项的配置

它与以下依赖项声明相匹配

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 还会删除状态已转移的所有每个配置的锁定文件。

迁移可以一次完成一个配置。只要单个锁定文件中没有该配置的信息,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")
}

运行具有锁定状态的构建

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

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

完整的验证如下

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

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

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

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

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

严格模式

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

宽松模式

在此模式中,依赖项锁定仍会固定动态版本,但对依赖项解析的更改不再是错误。

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

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

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

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

❯ gradle classes --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. 确保不再需要锁定的配置未配置锁定。

  2. 下次更新保存锁定状态时,Gradle 将自动清除其中的所有过时锁定状态。

Gradle 需要解析不再标记为锁定的配置,以检测可以放弃关联的锁定状态。

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

在可重复性不是主要目标的情况下可以使用依赖锁定。作为构建作者,您可能希望根据依赖项的来源(例如)来设置不同的依赖项版本更新频率。在这种情况下,忽略某些依赖项可能很方便,因为您始终希望对这些依赖项使用最新版本。一个示例是组织中的内部依赖项,它应始终使用最新版本,而第三方依赖项则具有不同的升级周期。

此功能可能会破坏可重复性,因此应谨慎使用。有些场景更适合利用不同的锁定模式使用不同的锁定文件名

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

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

表示法是<group>:<name>依赖项表示法,其中*可用作尾部通配符。有关更多详细信息,请参阅更新锁定文件的说明。请注意,不接受值*:*,因为它等同于禁用锁定。

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

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

  • 忽略依赖项并不意味着锁定状态会忽略其传递依赖项。

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

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

  • 如果依赖项存在于解析结果中,则在验证该解析是否与锁定状态匹配时将忽略它。

  • 最后,如果依赖项存在于解析结果中并且锁定状态已持久化,则它将不存在于编写的锁定状态中。

锁定限制

  • 锁定还不能应用于源依赖项。