Gradle 排除传递依赖

1、概览

Gradle 是一款构建自动化工具,用于管理和自动化应用程序的构建、测试和部署过程。

使用基于 GroovyKotlin 的 DSL(Domain-Specific Language)来定义构建任务,可以轻松地自动定义和管理项目中所需的依赖。

本文将带你了解在 Gradle 中排除传递依赖的几种方法。

2、传递依赖是什么?

假设我们使用的 A 依赖于另一个库 B。默认情况下,当我们包含 A 时,Gradle 会自动将 B 添加到项目的 classpath 中,这样,即使我们没有明确地将 B 添加为依赖,也可以在项目中使用 B 的代码。

来看一个实际的例子,在项目中定义 Google Guava 依赖:

dependencies {
    // ...
    implementation 'com.google.guava:guava:31.1-jre'
}

如果 Google Guava 与其他库存在依赖关系,那么 Gradle 会自动包含这些其他库。

要查看项目中使用的依赖项,可以使用如下命令将其打印出来:

./gradlew <module-name>:dependencies

假设我们的模块名为 excluding-transitive-dependencies

./gradlew excluding-transitive-dependencies:dependencies

输出如下:

testRuntimeClasspath - Runtime classpath of source set 'test'.
\--- com.google.guava:guava:31.1-jre
     +--- com.google.guava:failureaccess:1.0.1
     +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
     +--- com.google.code.findbugs:jsr305:3.0.2
     +--- org.checkerframework:checker-qual:3.12.0
     +--- com.google.errorprone:error_prone_annotations:2.11.0
     \--- com.google.j2objc:j2objc-annotations:1.3

我们可以看到一些我们没有明确定义的库,但 Gradle 自动添加了它们,因为 Google Guava 需要它们。

不过,有时我们也有充分的理由排除传递依赖。

3、为什么要排除传递依赖?

如下是排除传递依赖的几个原因:

  • 避免安全问题:例如,Firestore Firebase SDK 24.4.0Dagger 2.44Google Guava 31.1-jre 存在传递依赖关系,而后者存在 安全漏洞 问题。
  • 避免不必要的依赖:有些库可能会带来与我们的应用无关的依赖。
  • 减小应用体积:通过排除未使用的传递依赖,我们可以减少打包到应用中的库数量,从而减小输出文件(JAR、WAR、APK)的大小。我们还可以使用 ProGuard 等工具,通过移除未使用的代码、优化字节码、混淆类和方法名称以及移除不必要的资源,来大幅减小应用的大小。在不牺牲功能的前提下,这一过程可使应用程序更小、更快、更高效。

因此,Gradle 也提供了排除依赖的机制。

3.1、解决版本冲突

不建议通过排除传递依赖来解决版本冲突,因为 Gradle 已经有很好的 机制 来处理这个问题。

如果有两个或两个以上相同的依赖项,Gradle 只会选择其中一个。如果它们的版本不同,默认情况下,它会 选择最新版本。这一点可以通过日志看出来:

+--- org.hibernate.orm:hibernate-core:7.0.0.Beta1
|    +--- jakarta.persistence:jakarta.persistence-api:3.2.0-M2
|    +--- jakarta.transaction:jakarta.transaction-api:2.0.1
|    +--- org.jboss.logging:jboss-logging:3.5.0.Final <-------------------+ same version
|    +--- org.hibernate.models:hibernate-models:0.8.6                     |
|    |    +--- io.smallrye:jandex:3.1.2 -> 3.2.0    <------------------+  |
|    |    \--- org.jboss.logging:jboss-logging:3.5.0.Final +-----------|--+
|    +--- io.smallrye:jandex:3.2.0     +-------------------------------+ latest version
|    +--- com.fasterxml:classmate:1.5.1
|    |    \--- jakarta.activation:jakarta.activation-api:2.1.0 -> 2.1.1 <---+
|    +--- org.glassfish.jaxb:jaxb-runtime:4.0.2                             |
|    |    \--- org.glassfish.jaxb:jaxb-core:4.0.2                           |
|    |         +--- jakarta.xml.bind:jakarta.xml.bind-api:4.0.0 (*)         |
|    |         +--- jakarta.activation:jakarta.activation-api:2.1.1   +-----+ latest version
|    |         +--- org.eclipse.angus:angus-activation:2.0.0                

我们可以看到一些已识别的依赖是相同的。例如,org.jboss.logging:jboss-logging:3.5.0.Final 出现了两次,但由于它们的版本相同,Gradle 将只包含一份。

同时,jakarta.activation:jakarta.activation-api 有两个版本 - 2.1.02.1.1。Gradle 会选择最新版本,即 2.1.1。同样,io.smallrye:jandex 也会选择 3.2.0

但有时我们并不想使用最新版本,可以强制 Gradle 选择我们指定的版本:

implementation("io.smallrye:jandex") {
    version {
        strictly '3.1.2'
    }
}

如上,即使有另一个更新的版本,Gradle 仍会选择 3.1.2 版。

我们可以声明具有特定版本或 版本范围 的依赖,以定义我们项目可以使用的依赖的可接受版本。

4、排除传递依赖

4.1、排除 Group

当我们定义一个依赖,例如 Google Guava,其格式如下:

com.google.guava : guava : 31.1-jre
----------------   -----   --------
        ^            ^        ^
        |            |        |
      group        module   version

如果查看上文第 2 节中的输出,就会看到 Google Guava 依赖的五个模块。它们是 com.google.code.findbugscom.google.errorpronecom.google.guavacom.google.j2objcorg.checkerframework

我们要排除 com.google.guava Group,该 Group 包含 guavafailureaccesslistenablefuture 模块:

dependencies {
    // ...
    implementation ('com.google.guava:guava:31.1-jre') {
        exclude group: 'com.google.guava'
    }
}

这将排除 com.google.guava Group 中除 guava 之外的所有模块,因为 guava 是主模块。

4.2、排除特定模块

要排除特定的模块依赖,我们可以使用目标路径。例如,当我们使用 Hibernate 时,只需排除 org.glassfish.jaxb:txw2 模块即可:

dependencies {
    // ...
    implementation ('org.hibernate.orm:hibernate-core:7.0.0.Beta1') {
        exclude group: 'org.glassfish.jaxb', module : 'txw2'
    }
}

这意味着即使 Hibernate 依赖了 txw2 模块,我们也不会在项目中包含该模块。

4.3、排除多个模块

Gradle 还允许我们在一条依赖关系声明中排除多个模块:

dependencies {
    // ...
    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation ('org.junit.jupiter:junit-jupiter') {
        exclude group: 'org.junit.jupiter', module : 'junit-jupiter-api'
        exclude group: 'org.junit.jupiter', module : 'junit-jupiter-params'
        exclude group: 'org.junit.jupiter', module : 'junit-jupiter-engine'
    }
}

在本例中,我们从 org.junit-jupiter 依赖中排除了 junit-jupiter-apijunit-jupiter-paramsjunit-jupiter-engine 模块。

有了这种机制,我们就可以对更多的模块排除情况做同样的处理:

dependencies {
    // ...
    implementation('com.google.android.gms:play-services-mlkit-face-detection:17.1.0') {
        exclude group: 'androidx.annotation', module: 'annotation'
        exclude group: 'android.support.v4', module: 'core'
        exclude group: 'androidx.arch.core', module: 'core'
        exclude group: 'androidx.collection', module: 'collection'
        exclude group: 'androidx.coordinatorlayout', module: 'coordinatorlayout'
        exclude group: 'androidx.core', module: 'core'
        exclude group: 'androidx.viewpager', module: 'viewpager'
        exclude group: 'androidx.print', module: 'print'
        exclude group: 'androidx.localbroadcastmanager', module: 'localbroadcastmanager'
        exclude group: 'androidx.loader', module: 'loader'
        exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
        exclude group: 'androidx.lifecycle', module: 'lifecycle-livedata'
        exclude group: 'androidx.lifecycle', module: 'lifecycle-common'
        exclude group: 'androidx.fragment', module: 'fragment'
        exclude group: 'androidx.drawerlayout', module: 'drawerlayout'
        exclude group: 'androidx.legacy.content', module: 'legacy-support-core-utils'
        exclude group: 'androidx.cursoradapter', module: 'cursoradapter'
        exclude group: 'androidx.customview', module: 'customview'
        exclude group: 'androidx.documentfile.provider', module: 'documentfile'
        exclude group: 'androidx.interpolator', module: 'interpolator'
        exclude group: 'androidx.exifinterface', module: 'exifinterface'
    }
}

如上,从 Google ML 工具包依赖关系中排除了多个模块,以避免包含某些默认情况下已包含在项目中的模块。

4.4、排除所有传递模块

有时,我们可能只需要使用主模块,而不需要任何其他依赖项。或者,我们需要明确指定所使用的每个依赖(传递)的版本。

transitive = false 语句会告诉 Gradle 不要自动包含我们使用的库中的传递依赖:

dependencies {
    // ...
    implementation('org.hibernate.orm:hibernate-core:7.0.0.Beta1') {
        transitive = false
    }
}

这意味着只有 Hibernate Core 本身会被添加到项目中,而不会有任何其他依赖项。

4.5、在每个配置中排除

除了在依赖关系声明中排除传递依赖外,还可以在配置级别排除传递依赖。

我们可以通过 configurations.configureEach { } 配置集合中的每个元素。

该方法在 Gradle 4.9 及以上版本中可用,是 all() 的推荐 替代 方法。

如下:

dependencies { 
    // ...
    testImplementation 'org.mockito:mockito-core:3.+'
}

configurations.configureEach {
    exclude group: 'net.bytebuddy', module: 'byte-buddy-agent'
}

这意味着,在所有使用该依赖的配置中,都将排除 net.bytebuddy Group 中的 byte-buddy-agent 模块。

4.6、在特定配置中排除

有时,我们可能需要通过特定配置来排除依赖:

configurations.testImplementation {
    exclude group: 'org.junit.jupiter', module : 'junit-jupiter-engine'
}

configurations.testCompileClasspath {
    exclude group : 'com.google.j2objc', module : 'j2objc-annotations'
}

configurations.annotationProcessor {
    exclude group: 'com.google.guava'
}

如上,Gradle 允许以这种方式排除依赖。我们可以在 classpath 中对特定配置使用 exclude,如 testImplementationtestCompileClasspathannotationProcessor 等。

5、总结

排除传递依赖有三个主要原因:避免安全问题、避免使用不需要的库以及减少打包后应用的大小。

本文介绍了在 Gradle 中排除传递依赖的各种方法,从按 Group、特定模块或多个模块排除,到排除所有传递依赖模块。


Ref:https://www.baeldung.com/gradle-exclude-transitive-dependencies