在依赖关系图中,常见的情况是意外包含了同一 API 的多个实现,特别是对于像日志框架这样的库,各种传递依赖项选择了不同的绑定。

由于这些实现通常位于不同的组、构件和版本 (GAV) 坐标,构建工具通常无法检测到冲突。

为了解决这个问题,Gradle 引入了功能的概念。

理解功能

功能本质上是一种声明不同组件(依赖项)提供相同功能的方式。

在一个依赖关系图中,Gradle 不允许包含多个提供相同功能的组件。如果 Gradle 检测到两个组件提供相同的功能(例如,日志框架的不同绑定),它将构建失败并报错,指示冲突的模块。这确保了冲突的实现得到解决,避免了类路径上的问题。

例如,假设您依赖于两个不同的库来进行数据库连接池

dependencies {
    implementation("com.zaxxer:HikariCP:4.0.3")  // A popular connection pool
    implementation("org.apache.commons:commons-dbcp2:2.8.0")  // Another connection pool
}

configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("database:connection-pool") {
        select("com.zaxxer:HikariCP")
    }
}

在这种情况下,HikariCPcommons-dbcp2 都提供相同的功能(连接池)。如果两者都在类路径上,Gradle 将会失败。

由于只能使用一个,Gradle 的解析策略允许您选择 HikariCP,从而解决冲突。

理解功能坐标

功能(group, module, version) 三元组标识。

每个组件都基于其 GAV 坐标定义一个隐式功能:组、构件和版本。

例如,org.apache.commons:commons-lang3:3.8 模块具有一个隐式功能,其组为 org.apache.commons,名称为 commons-lang3,版本为 3.8

dependencies {
    implementation("org.apache.commons:commons-lang3:3.8")
}

重要的是要注意,功能是版本化的。

声明组件功能

为了尽早检测到冲突,通过规则声明组件功能非常有用,这样可以在构建期间捕获冲突,而不是在运行时。

一种常见的情况是,组件在新版本中被重定位到不同的坐标。

例如,ASM 库在 3.3.1 版本之前发布在 asm:asm 下,然后从 4.0 版本开始重定位到 org.ow2.asm:asm。将这两个版本都包含在类路径中是非法的,因为它们在不同的坐标下提供相同的功能。

由于每个组件都有一个基于其 GAV 坐标的隐式功能,我们可以通过使用规则来解决此冲突,该规则声明 asm:asm 模块提供 org.ow2.asm:asm 功能

build.gradle.kts
class AsmCapability : ComponentMetadataRule {
    override
    fun execute(context: ComponentMetadataContext) = context.details.run {
        if (id.group == "asm" && id.name == "asm") {
            allVariants {
                withCapabilities {
                    // Declare that ASM provides the org.ow2.asm:asm capability, but with an older version
                    addCapability("org.ow2.asm", "asm", id.version)
                }
            }
        }
    }
}
build.gradle
@CompileStatic
class AsmCapability implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        context.details.with {
            if (id.group == "asm" && id.name == "asm") {
                allVariants {
                    it.withCapabilities {
                        // Declare that ASM provides the org.ow2.asm:asm capability, but with an older version
                        it.addCapability("org.ow2.asm", "asm", id.version)
                    }
                }
            }
        }
    }
}

有了这个规则,如果依赖关系图中同时存在 asm:asm ( < = 3.3.1) 和 org.ow2.asm:asm (4.0+),构建将会失败。

Gradle 不会自动解决冲突,但这有助于您意识到问题的存在。建议将此类规则打包到插件中以供构建使用,从而允许用户决定使用哪个版本或修复类路径冲突。

在候选者之间选择

在某些时候,依赖关系图将包含不兼容的模块,或互斥的模块。

例如,您可能有不同的日志记录器实现,并且您需要选择一个绑定。功能帮助理解冲突,然后 Gradle 为您提供工具来解决冲突。

在不同的功能候选者之间选择

在上面的重定位示例中,Gradle 能够告诉您,您在类路径上有同一 API 的两个版本:“旧”模块和“重定位”模块。我们可以通过自动选择具有最高功能版本的组件来解决冲突

build.gradle.kts
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("org.ow2.asm:asm") {
        selectHighestVersion()
    }
}
build.gradle
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability('org.ow2.asm:asm') {
        selectHighestVersion()
    }
}

但是,选择最高功能版本冲突解决并不总是合适的。

例如,对于日志框架,我们使用哪个版本的日志框架并不重要。在这种情况下,我们显式选择 slf4j 作为首选选项

build.gradle.kts
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
        val toBeSelected = candidates.firstOrNull { it.id.let { id -> id is ModuleComponentIdentifier && id.module == "log4j-over-slf4j" } }
        if (toBeSelected != null) {
            select(toBeSelected)
        }
        because("use slf4j in place of log4j")
    }
}
build.gradle
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
        def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module == 'log4j-over-slf4j' }
        if (toBeSelected != null) {
            select(toBeSelected)
        }
        because 'use slf4j in place of log4j'
    }
}

如果您在类路径上有多个 slf4j 绑定,这种方法也适用;绑定基本上是不同的日志记录器实现,您只需要一个。但是,所选的实现可能取决于正在解析的配置。

例如,在测试环境中,轻量级的 slf4j-simple 日志记录器实现可能就足够了,而在生产环境中,可能更喜欢像 logback 这样更强大的解决方案。

只能选择依赖关系图中找到的模块进行解析。select 方法只接受当前候选者集合中的模块。如果所需的模块不是冲突的一部分,您可以选择不解决该特定冲突,实际上使其保持未解决状态。图中可能存在另一个冲突,其中包含您想要选择的模块。

如果没有为给定功能的所有冲突提供解决方案,构建将失败,因为为解析选择的模块未在图中找到。此外,调用 select(null) 将导致错误,应避免使用。

有关更多信息,请参阅 功能解析 API