使用动态依赖版本(例如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()
    }
}

锁定 buildscript 类路径配置

如果您将插件应用到您的构建中,您可能还想利用依赖锁定。要锁定用于脚本插件的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.lock文件
# 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*:将让所有模块的组以 update 开头org.springframework.spring且名称以springupdate开头

根据 Gradle 解析规则的规定,该解析可能会导致其他模块版本更新。

禁用依赖锁定

  1. 确保您不再需要锁定的配置未配置锁定。

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

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

忽略锁定状态的特定依赖关系

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

此功能可能会破坏再现性,应谨慎使用。在某些情况下,利用不同的锁定模式使用不同的锁定文件名称可以更好地服务。

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

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

该符号是<group>:<name>依赖符号,其中*可以用作尾随通配符。有关更多详细信息,请参阅有关更新锁定文件的说明。请注意,该值*:*不被接受,因为它相当于禁用锁定。

忽略依赖关系将产生以下影响:

  • 忽略的依赖关系适用于所有锁定的配置。该设置是项目范围内的。

  • 忽略依赖关系并不意味着锁状态忽略其传递依赖关系。

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

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

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

  • 最后,如果解析结果中存在依赖关系并且锁状态被持久化,则写入锁状态中将不存在依赖关系。

锁定限制

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