组件功能介绍

通常,依赖关系图会意外包含相同 API 的多个实现。这在日志框架中尤其常见,其中提供多个绑定,并且一个库在另一个传递依赖关系选择另一个绑定时选择一个绑定。由于这些实现位于不同的 GAV 坐标,因此构建工具通常无法找出这些库之间存在冲突。为了解决这个问题,Gradle 提供了功能的概念。

在单个依赖关系图中找到提供相同功能的两个组件是非法的。直观地说,这意味着如果 Gradle 在类路径中找到提供相同事物的两个组件,它将失败并显示一个错误,指示哪些模块存在冲突。在我们的示例中,这意味着日志框架的不同绑定提供了相同的功能。

功能坐标

功能(group, module, version) 三元组定义。每个组件都定义了一个与其 GAV 坐标(组、工件、版本)相对应的隐式功能。例如,org.apache.commons:commons-lang3:3.8 模块具有一个隐式功能,其组为 org.apache.commons,名称为 commons-lang3,版本为 3.8。重要的是要意识到功能是版本化的

声明组件功能

默认情况下,如果依赖关系图中的两个组件提供相同的功能,Gradle 将会失败。由于大多数模块目前发布时没有 Gradle 模块元数据,因此 Gradle 并不总是能够自动发现功能。然而,在构建过程中使用 *规则* 来声明组件功能,以便尽早发现冲突,而不是在运行时发现冲突,这很有意义。

一个典型的例子是,当一个组件在新版本中被重新定位到不同的坐标时。例如,ASM 库在版本 3.3.1 之前位于 asm:asm 坐标,然后从 4.0 开始更改为 org.ow2.asm:asm。在类路径中同时存在 ASM <= 3.3.1 和 4.0+ 是非法的,因为它们提供相同的功能,只是组件被重新定位了。由于每个组件都有一个与其 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)
                    }
                }
            }
        }
    }
}

现在,只要在同一个依赖关系图中找到这两个组件,构建就会 *失败*。

在这个阶段,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。

在这种情况下,我们可以通过显式选择 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 可能就足够了,但对于生产,slf4-over-log4j 可能更好。

解析只能有利于图中 *找到* 的模块。

select 方法只接受 *当前* 候选者中找到的模块。如果您要选择的模块不是冲突的一部分,您可以放弃执行选择,实际上不解决 *此* 冲突。图中可能存在针对相同功能的另一个冲突,并且将包含您要选择的模块。

如果在给定功能上所有冲突都没有给出解析,则构建将失败,因为为解析选择的模块根本不是图的一部分。

此外,select(null) 将导致错误,因此应避免。

有关更多信息,请查看 功能解析 API