覆盖传递依赖版本

Gradle 通过选择依赖关系图中找到的最新版本来解决任何依赖关系版本冲突。某些项目可能需要偏离默认行为并强制执行早期版本的依赖项,例如,如果项目的源代码依赖于比某些外部库更旧的依赖项 API。

强制依赖某个版本需要有意识的决定。如果外部库没有它们就无法正常运行,则更改传递依赖项的版本可能会导致运行时错误。考虑升级源代码以使用较新版本的库作为替代方法。

一般来说,强制依赖关系是为了降级依赖关系。降级可能有不同的用例:

  • 在最新版本中发现了一个错误

  • 您的代码依赖于不兼容二进制的较低版本

  • 您的代码不依赖于需要更高版本依赖项的代码路径

在所有情况下,最好的表达是您的代码严格依赖于传递的版本。使用strict versions,您将有效地依赖于您声明的版本,即使传递依赖另有说明。

严格依赖在某种程度上类似于 Maven 的最近优先策略,但也有细微的差别:

  • 严格依赖不会遇到排序问题:它们传递地应用于子图,并且声明依赖的顺序并不重要。

  • 冲突的严格依赖关系将触发您必须解决的构建失败

  • 严格依赖可以与丰富版本一起使用,这意味着最好用严格范围与单个首选版本相结合来表达需求。

假设一个项目使用HttpClient 库来执行 HTTP 调用。 HttpClient在版本 1.10 中引入Commons Codec作为传递依赖项。但是,该项目的生产源代码需要 Commons Codec 1.9 中的 API,该 API 在 1.10 中不再可用。可以通过在构建脚本中将其声明为 strict 来强制执行依赖版本:

build.gradle.kts
dependencies {
    implementation("org.apache.httpcomponents:httpclient:4.5.4")
    implementation("commons-codec:commons-codec") {
        version {
            strictly("1.9")
        }
    }
}
build.gradle
dependencies {
    implementation 'org.apache.httpcomponents:httpclient:4.5.4'
    implementation('commons-codec:commons-codec') {
        version {
            strictly '1.9'
        }
    }
}

使用严格版本的后果

使用严格版本必须仔细考虑,尤其是库作者。作为生产者,严格版本将有效地表现得像一个力量:版本声明优先于传递依赖图中找到的任何内容。特别是,严格版本将覆盖可传递找到的同一模块上的任何其他严格版本。

然而,对于消费者来说,在图形解析过程中仍然全局考虑严格版本,如果消费者不同意,可能会触发错误。

例如,假设您的项目B 严格依赖于C:1.0.现在,消费者A取决于BC:1.1

那么这将触发解析错误,因为A它表示需要,C:1.1在其子图中,严格需要。这意味着,如果您在严格约束中选择单个版本,那么该版本将无法再升级,除非消费者也对同一个模块设置了严格版本约束。B1.0

在上面的例子中,A不得不说它严格依赖于 1.1

因此,一个好的做法是,如果您使用严格版本,则应该用范围和此范围内的首选版本来表达它们。例如,B可能会说,而不是strictly 1.0,它严格取决于范围[1.0, 2.0[,但更喜欢 1.0。然后,如果消费者选择 1.1(或该范围内的任何其他版本),构建将不再失败(约束已解决)。

强制依赖与严格依赖

如果项目在配置级别需要特定版本的依赖项,则可以通过调用方法ResolutionStrategy.force(java.lang.Object[])来实现。

build.gradle.kts
configurations {
    "compileClasspath" {
        resolutionStrategy.force("commons-codec:commons-codec:1.9")
    }
}

dependencies {
    implementation("org.apache.httpcomponents:httpclient:4.5.4")
}
build.gradle
configurations {
    compileClasspath {
        resolutionStrategy.force 'commons-codec:commons-codec:1.9'
    }
}

dependencies {
    implementation 'org.apache.httpcomponents:httpclient:4.5.4'
}

排除传递依赖

虽然上一节展示了如何强制执行特定版本的传递依赖项,但本节介绍了排除作为完全删除传递依赖项的方法。

与强制依赖版本类似,完全排除依赖需要有意识的决定。如果外部库在没有传递依赖的情况下无法正常运行,则排除传递依赖可能会导致运行时错误。如果您使用排除,请确保您没有利用任何需要通过足够的测试覆盖率排除依赖项的代码路径。

可以在声明的依赖级别上排除传递依赖。排除项通过属性group和/或以键/值对的形式拼写出来module,如下例所示。有关更多信息,请参阅ModuleDependency.exclude(java.util.Map)

build.gradle.kts
dependencies {
    implementation("commons-beanutils:commons-beanutils:1.9.4") {
        exclude(group = "commons-collections", module = "commons-collections")
    }
}
build.gradle
dependencies {
    implementation('commons-beanutils:commons-beanutils:1.9.4') {
        exclude group: 'commons-collections', module: 'commons-collections'
    }
}

在此示例中,我们添加依赖项commons-beanutils但排除传递依赖项commons-collections。在我们的代码中,如下所示,我们仅使用 beanutils 库中的一种方法,PropertyUtils.setSimpleProperty().对于现有设置器使用此方法不需要commons-collections我们通过测试覆盖率验证的任何功能。

src/main/java/Main.java
import org.apache.commons.beanutils.PropertyUtils;

public class Main {
    public static void main(String[] args) throws Exception {
        Object person = new Person();
        PropertyUtils.setSimpleProperty(person, "name", "Bart Simpson");
        PropertyUtils.setSimpleProperty(person, "age", 38);
    }
}

实际上,我们表达的是我们只使用该库的一个子集,这不需要该commons-collection库。这可以看作是隐式定义了一个尚未被自身显式声明的特征变体commons-beanutils。然而,这样做会增加破坏未经测试的代码路径的风险。

例如,这里我们使用setSimpleProperty()方法来修改类中setter定义的属性Person,效果很好。如果我们尝试设置类中不存在的属性,我们应该会收到类似 的错误Unknown property on class Person。但是,因为错误处理路径使用了 中的类commons-collections,所以我们现在得到的错误是NoClassDefFoundError: org/apache/commons/collections/FastHashMap。因此,如果我们的代码更加动态,并且我们忘记充分覆盖错误情况,那么我们库的使用者可能会遇到意想不到的错误。

这只是说明潜在陷阱的示例。在实践中,较大的库或框架可能会带来大量的依赖项。如果这些库无法单独声明功能,并且只能以“全有或全无”的方式使用,则排除可能是将库缩减为实际所需的功能集的有效方法。

从好的方面来说,与 Maven 相比,Gradle 的排除处理考虑了整个依赖关系图。因此,如果某个库存在多个依赖项,则仅当所有依赖项都同意时才会执行排除。例如,如果我们将opencsv另一个依赖项添加到上面的项目中,该依赖项也依赖于commons-beanutilscommons-collection则不再排除它,因为opencsv它本身排除它。

build.gradle.kts
dependencies {
    implementation("commons-beanutils:commons-beanutils:1.9.4") {
        exclude(group = "commons-collections", module = "commons-collections")
    }
    implementation("com.opencsv:opencsv:4.6") // depends on 'commons-beanutils' without exclude and brings back 'commons-collections'
}
build.gradle
dependencies {
    implementation('commons-beanutils:commons-beanutils:1.9.4') {
        exclude group: 'commons-collections', module: 'commons-collections'
    }
    implementation 'com.opencsv:opencsv:4.6' // depends on 'commons-beanutils' without exclude and brings back 'commons-collections'
}

如果我们仍然想commons-collections排除它,因为我们组合使用commons-beanutilsopencsv不需要它,我们opencsv也需要将它从 的传递依赖中排除。

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

从历史上看,排除也被用作解决某些依赖管理系统不支持的其他问题的创可贴。然而,Gradle 提供了多种可能更适合解决特定用例的功能。您可以考虑研究以下功能:

  • 更新降级依赖项版本:如果依赖项的版本发生冲突,通常最好通过依赖项约束来调整版本,而不是尝试排除具有不需要的版本的依赖项。

  • 组件元数据规则:如果库的元数据明显错误,例如,如果它包含编译时永远不需要的编译时依赖项,则可能的解决方案是删除组件元数据规则中的依赖项。通过这种方式,你告诉 Gradle 两个模块之间的依赖关系是不需要的——即元数据是错误的——因此永远不应该考虑。如果您正在开发一个库,则必须注意此信息未发布,因此有时排除可能是更好的选择。

  • 解决互斥依赖冲突:您经常看到的通过排除解决的另一种情况是两个依赖项不能一起使用,因为它们代表同一事物(相同功能)的两种实现。一些流行的示例是冲突的日志 API 实现(如log4jlog4j-over-slf4j)或在不同版本中具有不同坐标的模块(如com.google.collectionsguava)。在这些情况下,如果 Gradle 不知道此信息,建议通过组件元数据规则添加缺少的功能信息,如声明组件功能部分中所述。即使您正在开发一个库,而您的消费者将不得不再次解决冲突,将决定权留给库的最终消费者通常是正确的解决方案。即,作为库作者,您不必决定您的消费者最终使用哪种日志记录实现。