1. 概述

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

它基于 GroovyKotlin 的领域特定语言(DSL)定义构建任务,使我们能轻松定义和管理项目所需的库依赖。

本教程将重点讨论在Gradle中排除传递性依赖的几种方法。

2. 什么是传递性依赖?

假设我们使用库A,而库A又依赖库B。那么B就是A的传递性依赖。默认情况下,当我们引入A时,Gradle会自动将B添加到项目的类路径中,这样我们就能在项目中使用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因为Guava需要它们而自动添加。

但有时我们可能有充分理由排除这些传递性依赖。

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

以下是几个常见原因:

  • 规避安全风险:例如 Firestore Firebase SDK 24.4.0 或 Dagger 2.44 传递性依赖 Google 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 <-------------------+ 相同版本
|    +--- 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     +-------------------------------+ 最新版本
|    +--- 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   +-----+ 最新版本
|    |         +--- org.eclipse.angus:angus-activation:2.0.0                

例如 org.jboss.logging:jboss-logging:3.5.0.Final 出现两次,因版本相同只保留一份。而 jakarta.activation:jakarta.activation-api 存在 2.1.0 和 2.1.1 两个版本,Gradle选择最新版 2.1.1。io.smallrye:jandex 同理选择 3.2.0。

但有时我们不想用最新版,可强制指定版本

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

即使存在更新版本,Gradle仍会使用 3.1.2。

也可通过版本范围声明可接受的依赖版本。

4. 排除传递性依赖

我们可在多种场景下排除传递性依赖。下面结合常见库实例说明。

4.1. 按组排除

依赖声明格式如下(以Guava为例):

com.google.guava : guava : 31.1-jre
----------------   -----   --------
        ^            ^        ^
        |            |        |
      组(group)    模块(module) 版本(version)

根据第2节的输出,Guava依赖五个组:com.google.code.findbugscom.google.errorpronecom.google.guavacom.google.j2objcorg.checkerframework

排除 com.google.guava 组(包含 guavafailureaccesslistenablefuture 模块):

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

这将排除 com.google.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 组下的三个模块。

类似地,可排除更多模块(如Google ML Kit的冗余依赖):

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'
    }
}

此操作可避免重复引入项目中已存在的模块。

4.4. 排除所有传递性模块

有时我们只需主模块,或想显式控制每个依赖的版本。

使用 transitive = false 告知Gradle不自动引入传递性依赖:

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

这样只会添加Hibernate Core本身,不包含其任何依赖。

4.5. 全局配置排除

除在依赖声明中排除,还可在配置(configuration)层面全局排除。

使用 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:byte-buddy-agent

4.6. 特定配置排除

有时需要针对特定配置排除依赖。Gradle同样支持:

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'
}

可在 testImplementationtestCompileClasspathannotationProcessor 等特定配置的类路径中使用 exclude

5. 总结

排除传递性依赖主要有三个原因:规避安全风险、移除无用依赖、减小应用体积。

本文介绍了Gradle中排除传递性依赖的多种方式:按组排除、排除特定模块、排除多个模块、排除所有传递性模块,以及在配置层面全局或特定排除。但需谨慎决策,避免过度排除导致运行时问题

完整源代码见GitHub仓库


原始标题:Excluding Transitive Dependencies in Gradle | Baeldung