本节介绍 Gradle 提供的直接影响依赖解析引擎行为的机制。与本章中介绍的其他概念(如 依赖约束组件元数据规则)不同,这些概念都是解析的输入,以下机制允许您编写直接注入解析引擎的规则。因此,它们可以被视为蛮力解决方案,可能会隐藏未来的问题(例如,如果添加了新的依赖项)。因此,一般建议是,只有在其他方法不足的情况下才使用以下机制。如果您正在编写 ,您应该始终优先使用 依赖约束,因为它们是为您的使用者发布的。

使用依赖解析规则

对于每个解析的依赖项,都会执行一个依赖解析规则,它提供了一个强大的 API,用于在解析依赖项之前操作请求的依赖项。该功能目前提供了更改请求的依赖项的组、名称和/或版本的能力,允许在解析期间用完全不同的模块替换依赖项。

依赖解析规则提供了一种非常强大的方式来控制依赖解析过程,并且可以用来实现各种依赖管理中的高级模式。下面概述了一些这些模式。有关更多信息和代码示例,请参阅 API 文档中的 ResolutionStrategy 类。

实现自定义版本方案

在某些企业环境中,可以在 Gradle 构建中声明的模块版本列表由外部维护和审核。依赖解析规则为这种模式提供了一种简洁的实现方式。

  • 在构建脚本中,开发人员使用模块组和名称声明依赖项,但使用占位符版本,例如:default

  • default 版本通过依赖解析规则解析为特定版本,该规则在企业批准的模块目录中查找版本。

这种规则实现可以很好地封装在企业插件中,并在组织中的所有构建之间共享。

build.gradle.kts
configurations.all {
    resolutionStrategy.eachDependency {
        if (requested.version == "default") {
            val version = findDefaultVersionInCatalog(requested.group, requested.name)
            useVersion(version.version)
            because(version.because)
        }
    }
}

data class DefaultVersion(val version: String, val because: String)

fun findDefaultVersionInCatalog(group: String, name: String): DefaultVersion {
    //some custom logic that resolves the default version into a specific version
    return DefaultVersion(version = "1.0", because = "tested by QA")
}
build.gradle
configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        if (details.requested.version == 'default') {
            def version = findDefaultVersionInCatalog(details.requested.group, details.requested.name)
            details.useVersion version.version
            details.because version.because
        }
    }
}

def findDefaultVersionInCatalog(String group, String name) {
    //some custom logic that resolves the default version into a specific version
    [version: "1.0", because: 'tested by QA']
}

使用替换拒绝特定版本

依赖解析规则提供了一种机制来拒绝依赖项的特定版本并提供替换版本。如果某个依赖项版本已损坏且不应使用,这将非常有用,其中依赖项解析规则会导致此版本被替换为已知的良好版本。损坏模块的一个示例是声明对无法在任何公共存储库中找到的库的依赖项的模块,但还有许多其他原因导致特定模块版本不受欢迎,而更喜欢其他版本。

在下面的示例中,假设版本 1.2.1 包含重要的修复程序,并且应始终优先于 1.2 使用。提供的规则将强制执行这一点:每次遇到版本 1.2 时,它将被替换为 1.2.1。请注意,这与上面描述的强制版本不同,因为此模块的任何其他版本都不会受到影响。这意味着如果此版本也被传递依赖,则“最新”冲突解决策略仍然会选择版本 1.3

build.gradle.kts
configurations.all {
    resolutionStrategy.eachDependency {
        if (requested.group == "org.software" && requested.name == "some-library" && requested.version == "1.2") {
            useVersion("1.2.1")
            because("fixes critical bug in 1.2")
        }
    }
}
build.gradle
configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        if (details.requested.group == 'org.software' && details.requested.name == 'some-library' && details.requested.version == '1.2') {
            details.useVersion '1.2.1'
            details.because 'fixes critical bug in 1.2'
        }
    }
}

使用 丰富版本约束reject 指令存在区别:如果在依赖图中发现被拒绝的版本,丰富版本将导致构建失败,或者在使用动态依赖时选择非拒绝版本。在这里,我们操作请求的版本,以便在找到被拒绝的版本时选择不同的版本。换句话说,这是对被拒绝版本的解决方案,而丰富版本约束允许声明意图(你不应该使用此版本)。

使用模块替换规则

最好用 功能冲突 来表达模块冲突。但是,如果没有声明此类规则,或者你正在使用不支持功能的 Gradle 版本,Gradle 提供了工具来解决这些问题。

模块替换规则允许构建声明一个旧库已被一个新库替换。一个很好的例子是 google-collections -> guava 迁移,其中一个新库替换了旧库。创建 google-collections 的团队决定将模块名称从 com.google.collections:google-collections 更改为 com.google.guava:guava。这在行业中是一个合法的场景:团队需要能够更改他们维护的产品的名称,包括模块坐标。模块坐标的重命名会影响冲突解决。

为了解释对冲突解决的影响,让我们考虑 google-collections -> guava 场景。可能会发生两种库都被拉入同一个依赖图中。例如,我们的项目依赖于 guava,但我们的依赖项中的一些拉入了 google-collections 的旧版本。这会导致运行时错误,例如在测试或应用程序执行期间。Gradle 不会自动解决 google-collections -> guava 冲突,因为它不被视为版本冲突。这是因为两种库的模块坐标完全不同,并且当 groupmodule 坐标相同但依赖图中存在不同的版本时,会激活冲突解决(有关更多信息,请参阅有关冲突解决的部分)。解决此问题的传统方法是

  • 声明排除规则以避免将 google-collections 拉入图中。这可能是最流行的方法。

  • 避免依赖引入遗留库。

  • 如果新版本不再引入遗留库,请升级依赖版本。

  • 降级到 google-collections。不推荐,只是为了完整性而提及。

传统方法有效,但不够通用。例如,一个组织希望解决所有项目中的 google-collections -> guava 冲突解决问题。可以声明某个模块被另一个模块替换。这使组织能够将模块替换信息包含在企业插件套件中,并为企业中所有由 Gradle 驱动的项目整体解决问题。

build.gradle.kts
dependencies {
    modules {
        module("com.google.collections:google-collections") {
            replacedBy("com.google.guava:guava", "google-collections is now part of Guava")
        }
    }
}
build.gradle
dependencies {
    modules {
        module("com.google.collections:google-collections") {
            replacedBy("com.google.guava:guava", "google-collections is now part of Guava")
        }
    }
}

有关更多示例和详细 API,请参阅 ComponentMetadataHandler 的 DSL 参考。

当我们声明 google-collectionsguava 替换时会发生什么?Gradle 可以使用此信息进行冲突解决。Gradle 将认为 guava 的任何版本都比 google-collections 的任何版本更新/更好。此外,Gradle 将确保类路径/解析文件列表中只存在 guava jar。请注意,如果依赖图中只出现 google-collections(例如,没有 guava),Gradle 不会急于用 guava 替换它。模块替换是 Gradle 用于解决冲突的信息。如果没有冲突(例如,图中只有 google-collections 或只有 guava),则不会使用替换信息。

目前无法声明给定模块被一组模块替换。但是,可以声明多个模块被单个模块替换。

使用依赖替换规则

依赖替换规则的工作原理类似于依赖解析规则。事实上,依赖解析规则的许多功能都可以用依赖替换规则来实现。它们允许项目和模块依赖关系被透明地替换为指定的替换项。与依赖解析规则不同,依赖替换规则允许项目和模块依赖关系被互换地替换。

向配置添加依赖替换规则会改变该配置解析的时间。 配置不会在第一次使用时解析,而是在构建任务图时解析。如果在任务执行期间进一步修改配置,或者如果配置依赖于在执行另一个任务期间发布的模块,这可能会产生意想不到的后果。

解释

  • Configuration 可以被声明为任何 Task 的输入,并且该配置可以在解析时包含项目依赖关系。

  • 如果项目依赖关系是 Task 的输入(通过配置),那么必须将构建项目工件的任务添加到任务依赖关系中。

  • 为了确定任务的输入项目依赖项,Gradle 需要解析 Configuration 输入。

  • 由于 Gradle 任务图在任务执行开始后就固定了,因此 Gradle 需要在执行任何任务之前执行此解析。

在没有依赖项替换规则的情况下,Gradle 知道外部模块依赖项永远不会传递地引用项目依赖项。这使得通过简单的图遍历很容易确定配置的完整项目依赖项集。有了此功能,Gradle 就无法再做出此假设,必须执行完全解析才能确定项目依赖项。

用项目依赖项替换外部模块依赖项

依赖项替换的一种用例是使用本地开发的模块版本来代替从外部存储库下载的版本。这对于测试本地修补的依赖项版本可能很有用。

要替换的模块可以声明为指定版本或不指定版本。

build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(module("org.utils:api"))
            .using(project(":api")).because("we work with the unreleased development version")
        substitute(module("org.utils:util:2.5")).using(project(":util"))
    }
}
build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute module("org.utils:api") using project(":api") because "we work with the unreleased development version"
        substitute module("org.utils:util:2.5") using project(":util")
    }
}

请注意,被替换的项目必须包含在多项目构建中(通过 settings.gradle)。依赖项替换规则负责用项目依赖项替换模块依赖项并连接任何任务依赖项,但不会隐式地将项目包含在构建中。

用模块替换替换项目依赖项

使用替换规则的另一种方法是在多项目构建中用模块替换项目依赖项。这对于使用大型多项目构建来加速开发可能很有用,因为它允许从存储库下载项目依赖项的子集,而不是构建它们。

用作替换的模块必须声明为指定版本。

build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(project(":api"))
            .using(module("org.utils:api:1.3")).because("we use a stable version of org.utils:api")
    }
}
build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute project(":api") using module("org.utils:api:1.3") because "we use a stable version of org.utils:api"
    }
}

当项目依赖项被模块依赖项替换时,该项目仍然包含在整个多项目构建中。但是,为了解决依赖的Configuration,不会执行构建被替换依赖项的任务。

有条件地替换依赖项

依赖项替换的常见用例是允许在多项目构建中更灵活地组装子项目。这对于开发外部依赖项的本地修补版本或构建大型多项目构建中的模块子集很有用。

以下示例使用依赖项替换规则来替换任何具有组org.example的模块依赖项,但前提是能够找到与依赖项名称匹配的本地项目。

build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution.all {
        requested.let {
            if (it is ModuleComponentSelector && it.group == "org.example") {
                val targetProject = findProject(":${it.module}")
                if (targetProject != null) {
                    useTarget(targetProject)
                }
            }
        }
    }
}
build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
        if (dependency.requested instanceof ModuleComponentSelector && dependency.requested.group == "org.example") {
            def targetProject = findProject(":${dependency.requested.module}")
            if (targetProject != null) {
                dependency.useTarget targetProject
            }
        }
    }
}

请注意,被替换的项目必须包含在多项目构建中(通过settings.gradle)。依赖项替换规则负责用项目依赖项替换模块依赖项,但不会隐式将项目包含在构建中。

用另一个变体替换依赖项

Gradle 的依赖项管理引擎是 变体感知的,这意味着对于单个组件,引擎可以选择不同的工件和传递依赖项。

选择什么取决于消费者配置的属性和生产方找到的变体的属性。但是,某些特定依赖项可能会覆盖配置本身的属性。这通常是使用 Java 平台插件 时的情况:此插件构建一种特殊类型的组件,称为“平台”,可以通过将组件类别属性设置为platform来寻址,这与针对库的典型依赖项相反。

因此,您可能会遇到想要用常规依赖项替换平台依赖项,或反之的情况。

用属性替换依赖项

假设您想用常规依赖项替换平台依赖项。这意味着您正在使用的库声明了类似以下内容

lib/build.gradle.kts
dependencies {
    // This is a platform dependency but you want the library
    implementation(platform("com.google.guava:guava:28.2-jre"))
}
lib/build.gradle
dependencies {
    // This is a platform dependency but you want the library
    implementation platform('com.google.guava:guava:28.2-jre')
}

platform 关键字实际上是带属性的依赖项的简写。如果我们想用常规依赖项替换此依赖项,那么我们需要精确地选择具有platform 属性的依赖项。

这可以通过使用替换规则来完成

consumer/build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(platform(module("com.google.guava:guava:28.2-jre")))
            .using(module("com.google.guava:guava:28.2-jre"))
    }
}
consumer/build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(platform(module('com.google.guava:guava:28.2-jre'))).
            using module('com.google.guava:guava:28.2-jre')
    }
}

相同的规则使用platform关键字会尝试用常规依赖项替换常规依赖项,这不是你想要的,所以理解替换规则适用于依赖项规范很重要:它将请求的依赖项(substitute XXX)与替换项(using YYY)匹配。

你可以在请求的依赖项替换项上添加属性,并且替换不限于platform:你实际上可以使用variant符号指定整个依赖项属性集。以下规则完全等同于上面的规则

consumer/build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(variant(module("com.google.guava:guava:28.2-jre")) {
            attributes {
                attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.REGULAR_PLATFORM))
            }
        }).using(module("com.google.guava:guava:28.2-jre"))
    }
}
consumer/build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute variant(module('com.google.guava:guava:28.2-jre')) {
            attributes {
                attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.REGULAR_PLATFORM))
            }
        } using module('com.google.guava:guava:28.2-jre')
    }
}

请参考替换 DSL API 文档以获取变体替换 API 的完整参考。

复合构建中,你必须匹配确切请求的依赖项属性的规则不适用:在使用复合构建时,Gradle 会自动匹配请求的属性。换句话说,如果你包含另一个构建,则隐式地意味着你正在用包含构建中的等效变体替换被替换模块的所有变体

用具有功能的依赖项替换依赖项

属性替换类似,Gradle 允许你用具有或不具有功能的另一个依赖项替换具有或不具有功能的依赖项。

例如,假设你需要用它的测试夹具替换常规依赖项。你可以使用以下依赖项替换规则来实现这一点

build.gradle.kts
configurations.testCompileClasspath {
    resolutionStrategy.dependencySubstitution {
        substitute(module("com.acme:lib:1.0")).using(variant(module("com.acme:lib:1.0")) {
            capabilities {
                requireCapability("com.acme:lib-test-fixtures")
            }
        })
    }
}
build.gradle
configurations.testCompileClasspath {
    resolutionStrategy.dependencySubstitution {
        substitute(module('com.acme:lib:1.0'))
            .using variant(module('com.acme:lib:1.0')) {
            capabilities {
                requireCapability('com.acme:lib-test-fixtures')
            }
        }
    }
}

在请求的依赖项上声明的替换规则中的功能构成依赖项匹配规范的一部分,因此需要功能的依赖项将不会匹配。

请参考替换 DSL API 文档以获取变体替换 API 的完整参考。

用分类器或工件替换依赖项

虽然外部模块通常通过其组/工件/版本坐标进行寻址,但这些模块通常会发布一些额外的工件,你可能希望用它们来代替主工件。这通常适用于分类工件,但你可能还需要选择具有不同文件类型或扩展名的工件。Gradle 不鼓励在依赖项中使用分类器,而是更倾向于将这些工件建模为模块的附加变体。与分类工件相比,使用变体有很多优势,包括但不限于这些工件的依赖项集不同。

然而,为了帮助桥接这两个模型,Gradle 提供了在替换规则中更改或删除分类器的方法。

consumer/build.gradle.kts
dependencies {
    implementation("com.google.guava:guava:28.2-jre")
    implementation("co.paralleluniverse:quasar-core:0.8.0")
    implementation(project(":lib"))
}
consumer/build.gradle
dependencies {
    implementation 'com.google.guava:guava:28.2-jre'
    implementation 'co.paralleluniverse:quasar-core:0.8.0'
    implementation project(':lib')
}

在上面的示例中,对 quasar 的一级依赖项让我们认为 Gradle 会解析 quasar-core-0.8.0.jar,但事实并非如此:构建将以以下消息失败

Execution failed for task ':resolve'.
> Could not resolve all files for configuration ':runtimeClasspath'.
   > Could not find quasar-core-0.8.0-jdk8.jar (co.paralleluniverse:quasar-core:0.8.0).
     Searched in the following locations:
         https://repo1.maven.org/maven2/co/paralleluniverse/quasar-core/0.8.0/quasar-core-0.8.0-jdk8.jar

这是因为另一个项目 lib 上存在一个依赖项,它本身依赖于 quasar-core 的不同版本。

lib/build.gradle.kts
dependencies {
    implementation("co.paralleluniverse:quasar-core:0.7.10:jdk8")
}
lib/build.gradle
dependencies {
    implementation "co.paralleluniverse:quasar-core:0.7.10:jdk8"
}

发生的情况是,Gradle 会在 quasar-core 0.8.0 和 quasar-core 0.7.10 之间执行冲突解析。由于 0.8.0 更高,我们选择此版本,但 lib 中的依赖项有一个分类器 jdk8,而此分类器在 0.8.0 版本中不再存在

要解决此问题,您可以要求 Gradle 不带分类器解析这两个依赖项。

consumer/build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(module("co.paralleluniverse:quasar-core"))
            .using(module("co.paralleluniverse:quasar-core:0.8.0"))
            .withoutClassifier()
    }
}
consumer/build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute module('co.paralleluniverse:quasar-core') using module('co.paralleluniverse:quasar-core:0.8.0') withoutClassifier()
    }
}

此规则有效地将图中找到的任何对 quasar-core 的依赖项替换为不带分类器的依赖项。

或者,可以选择特定分类器的依赖项,或者,对于更具体的用例,用非常具体的工件(类型、扩展名和分类器)替换。

有关更多信息,请参阅以下 API 文档

禁用传递解析

默认情况下,Gradle 会解析依赖项元数据指定的全部传递依赖项。有时这种行为可能不理想,例如,如果元数据不正确或定义了大量的传递依赖项图。您可以通过将 ModuleDependency.setTransitive(boolean) 设置为 false 来告诉 Gradle 禁用依赖项的传递依赖项管理。这样,只会为声明的依赖项解析主工件。

build.gradle.kts
dependencies {
    implementation("com.google.guava:guava:23.0") {
        isTransitive = false
    }
}
build.gradle
dependencies {
    implementation('com.google.guava:guava:23.0') {
        transitive = false
    }
}
禁用传递依赖项解析可能需要您在构建脚本中声明必要的运行时依赖项,否则这些依赖项将自动解析。如果不这样做,可能会导致运行时类路径问题。

项目可以决定完全禁用传递依赖解析。您要么不想依赖于发布到已使用存储库的元数据,要么想完全控制图中的依赖项。有关更多信息,请参见 Configuration.setTransitive(boolean)

build.gradle.kts
configurations.all {
    isTransitive = false
}

dependencies {
    implementation("com.google.guava:guava:23.0")
}
build.gradle
configurations.all {
    transitive = false
}

dependencies {
    implementation 'com.google.guava:guava:23.0'
}

在解析之前更改配置依赖项

有时,插件可能希望在解析配置之前修改其依赖项。withDependencies 方法允许以编程方式添加、删除或修改依赖项。

build.gradle.kts
configurations {
    create("implementation") {
        withDependencies {
            val dep = this.find { it.name == "to-modify" } as ExternalModuleDependency
            dep.version {
                strictly("1.2")
            }
        }
    }
}
build.gradle
configurations {
    implementation {
        withDependencies { DependencySet dependencies ->
            ExternalModuleDependency dep = dependencies.find { it.name == 'to-modify' } as ExternalModuleDependency
            dep.version {
                strictly "1.2"
            }
        }
    }
}

设置默认配置依赖项

可以为配置配置默认依赖项,如果未为配置显式设置任何依赖项,则使用这些依赖项。此功能的主要用例是为开发使用用户可能覆盖的版本化工具的插件。通过指定默认依赖项,插件仅当用户未指定要使用的特定版本时才可以使用工具的默认版本。

build.gradle.kts
configurations {
    create("pluginTool") {
        defaultDependencies {
            add(project.dependencies.create("org.gradle:my-util:1.0"))
        }
    }
}
build.gradle
configurations {
    pluginTool {
        defaultDependencies { dependencies ->
            dependencies.add(project.dependencies.create("org.gradle:my-util:1.0"))
        }
    }
}

完全从配置中排除依赖项

类似于 在依赖项声明中排除依赖项,您可以使用 Configuration.exclude(java.util.Map) 完全排除特定配置的传递依赖项。这将自动排除在配置上声明的所有依赖项的传递依赖项。

build.gradle.kts
configurations {
    "implementation" {
        exclude(group = "commons-collections", module = "commons-collections")
    }
}

dependencies {
    implementation("commons-beanutils:commons-beanutils:1.9.4")
    implementation("com.opencsv:opencsv:4.6")
}
build.gradle
configurations {
    implementation {
        exclude group: 'commons-collections', module: 'commons-collections'
    }
}

dependencies {
    implementation 'commons-beanutils:commons-beanutils:1.9.4'
    implementation 'com.opencsv:opencsv:4.6'
}

将依赖项与存储库匹配

Gradle 公开了 API 来声明存储库可能包含或可能不包含的内容。此功能提供了对哪些存储库提供哪些工件的细粒度控制,这可能是控制依赖项来源的一种方式。

转到 有关存储库内容过滤的章节,以详细了解此功能。

启用 Ivy 动态解析模式

Gradle 的 Ivy 存储库实现支持等效于 Ivy 的动态解析模式。通常,Gradle 将使用 ivy.xml 文件中包含的每个依赖项定义的 rev 属性。在动态解析模式下,Gradle 将改为优先使用给定依赖项定义的 revConstraint 属性而不是 rev 属性。如果 revConstraint 属性不存在,则改为使用 rev 属性。

要启用动态解析模式,您需要在存储库定义上设置适当的选项。下面显示了一些示例。请注意,动态解析模式仅适用于 Gradle 的 Ivy 存储库。它不适用于 Maven 存储库或自定义 Ivy DependencyResolver 实现。

build.gradle.kts
// Can enable dynamic resolve mode when you define the repository
repositories {
    ivy {
        url = uri("http://repo.mycompany.com/repo")
        resolve.isDynamicMode = true
    }
}

// Can use a rule instead to enable (or disable) dynamic resolve mode for all repositories
repositories.withType<IvyArtifactRepository> {
    resolve.isDynamicMode = true
}
build.gradle
// Can enable dynamic resolve mode when you define the repository
repositories {
    ivy {
        url "http://repo.mycompany.com/repo"
        resolve.dynamicMode = true
    }
}

// Can use a rule instead to enable (or disable) dynamic resolve mode for all repositories
repositories.withType(IvyArtifactRepository) {
    resolve.dynamicMode = true
}