Overriding transitive dependency versions

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

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

通常,执行强制依赖关系可以降级依赖关系. 降级可能会有不同的用例:

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

  • 您的代码取决于与二进制不兼容的较低版本

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

在所有情况下,最好的表述是您的代码严格依赖于可传递代码的版本. 使用严格版本 ,即使传递依赖项另有说明,您也将有效地依赖声明的版本.

严格的依赖关系在某种程度上类似于Maven 最接近的优先策略,但存在细微的差异:

  • 严格的依赖关系不会遇到排序问题:它们可传递地应用于子图,并且声明顺序依赖关系也无关紧要.

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

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

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

例子1.设置一个严格的版本
build.gradle
dependencies {
    implementation 'org.apache.httpcomponents:httpclient:4.5.4'
    implementation('commons-codec:commons-codec') {
        version {
            strictly '1.9'
        }
    }
}
build.gradle.kts
dependencies {
    implementation("org.apache.httpcomponents:httpclient:4.5.4")
    implementation("commons-codec:commons-codec") {
        version {
            strictly("1.9")
        }
    }
}

Consequences of using strict versions

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

但是,对于使用者,在图形解析期间仍会严格考虑严格版本,如果使用者不同意,则可能会引发错误 .

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

然后这将引发解析错误,因为A表示需要C:1.1B 在其子图中严格要求1.0 . 这意味着,如果您在严格限制中选择单个版本 ,则该版本将无法再升级 ,除非使用者也对同一模块设置了严格版本限制.

在上面的示例中, A必须说它严格取决于1.1 .

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

Forced dependencies vs strict dependencies

不推荐使用通过ExternalDependency.setForce(boolean)强制依赖,并且不再建议使用:强制依赖存在一个排序问题,该问题很难诊断,并且无法与其他丰富版本约束一起很好地工作. 您应该选择严格的版本 . 如果要编写和发布 ,则还需要注意没有发布force .

如果由于某种原因不能使用严格版本 ,则可以强制依赖项执行此操作:

例子2.强制执行一个依赖版本
build.gradle
dependencies {
    implementation 'org.apache.httpcomponents:httpclient:4.5.4'
    implementation('commons-codec:commons-codec:1.9') {
        force = true
    }
}
build.gradle.kts
dependencies {
    implementation("org.apache.httpcomponents:httpclient:4.5.4")
    implementation("commons-codec:commons-codec:1.9") {
        isForce = true
    }
}

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

例子3.在配置级别上执行一个依赖版本
build.gradle
configurations {
    compileClasspath {
        resolutionStrategy.force 'commons-codec:commons-codec:1.9'
    }
}

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

dependencies {
    implementation("org.apache.httpcomponents:httpclient:4.5.4")
}

Excluding transitive dependencies

尽管上一节显示了如何强制实施某个版本的可传递依赖项,但本节将排除作为完全删除可传递依赖项的一种方式.

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

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

例子4.排除特定依赖声明的传递依赖
build.gradle
dependencies {
    implementation('commons-beanutils:commons-beanutils:1.9.4') {
        exclude group: 'commons-collections', module: 'commons-collections'
    }
}
build.gradle.kts
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任何功能.

例子5.使用beanutils库中的工具
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()方法来修改Person类中setter定义的属性,效果很好. 如果尝试设置类中不存在的属性,则应该 Unknown property on class Person收到类似Unknown property on class Person的错误. 但是,由于错误处理路径使用commons-collections的类,所以我们现在得到的错误是NoClassDefFoundError: org/apache/commons/collections/FastHashMap . 因此,如果我们的代码更具动态性,而我们忘记了充分涵盖错误情况,则我们库的使用者可能会遇到意外错误.

这仅仅是说明潜在陷阱的一个示例. 实际上,较大的库或框架会带来大量的依赖关系. 如果这些库未能分别声明功能,并且只能以"全有或全无"的方式使用,则排除是将库缩减为实际所需功能集的有效方法.

从好的方面来说,与Maven相比,Gradle的排除处理考虑了整个依赖关系图. 因此,如果库上有多个依赖项,则只有在所有依赖项都对它们达成一致的情况下才执行排除. 举例来说,如果我们添加opencsv作为另一个依赖于我们上面的项目,这也取决于commons-beanutilscommons-collection不再排除在外,因为opencsv本身并不排除它.

示例6.仅当所有依赖项声明均同意排除项时,排除项才适用
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'
    }
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'
}

If we still want to have commons-collections excluded, because our combined usage of commons-beanutils and opencsv does not need it, we need to exclude it from the transitive dependencies of opencsv as well.

例子7.为多个依赖项声明排除传递依赖项
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'
        }
    }
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")
        }
    }

从历史上看,排除项还用作解决某些依赖项管理系统不支持的其他问题的工具. 但是,Gradle提供了可能更适合解决特定用例的各种功能. 您可以考虑研究以下功能:

  • 更新降级依赖性版本:如果依赖性版本冲突,通常最好通过依赖性约束来调整版本,而不是尝试用不需要的版本排除依赖性.

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

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