本节介绍 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'
        }
    }
}

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

使用模块替换规则

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

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

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

  • 声明排除规则以避免拉入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")
        }
    }
}

当我们声明google-collections被 替换时会发生什么guava? Gradle 可以使用此信息来解决冲突。 Gradle 会考虑每个版本guava比任何版本更新/更好的google-collections.此外,Gradle 将确保类路径/已解析文件列表中仅存在 guava jar。请注意,如果仅google-collections出现在依赖关系图中(例如 no guava),Gradle 不会急切地将其替换为guava。模块替换是 Gradle 用于解决冲突的信息。如果不存在冲突(例如,仅google-collections或仅guava在图中),则不使用替换信息。

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

使用依赖替换规则

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

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

解释:

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

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

  • 为了确定作为任务输入的项目依赖项,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

有条件地替换依赖项

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

以下示例使用依赖项替换规则将任何模块依赖项替换为 group 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')
}

该关键字实际上是具有attribute 的依赖关系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')
    }
}

请参阅Substitution 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')
            }
        }
    }
}

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

请参阅Substitution 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-core0.8.0 和quasar-core0.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)设置为 来告诉 Gradle 禁用依赖项的传递依赖项管理false。因此,只有主要工件将被解析为声明的依赖项。

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 将为文件rev中包含的每个依赖项定义使用该属性ivy.xml。在动态解析模式下,Gradle 将优先选择该revConstraint属性而rev不是给定依赖项定义的属性。如果该revConstraint属性不存在,rev则使用该属性。

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

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
}