Introduction to component capabilities

依赖图通常会偶然包含同一API的多个实现. 这在日志记录框架中尤其常见,在日志记录框架中有多个绑定可用,并且当另一个传递依赖项选择另一个时,一个库选择一个绑定. 由于这些实现位于不同的GAV坐标下,因此构建工具通常无法找出这些库之间是否存在冲突. 为了解决这个问题,Gradle提供了功能的概念.

在单个依赖关系图中找到两个提供相同功能的组件是非法的. 从直觉上讲,这意味着如果Gradle在类路径上找到两个提供相同功能的组件,它将失败并显示一个错误,指出哪些模块发生冲突. 在我们的示例中,这意味着日志记录框架的不同绑定提供了相同的功能.

Capability coordinates

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

Declaring component capabilities

默认情况下,如果依赖关系图中的两个组件提供相同的功能,则Gradle将失败. 因为当前发布的大多数模块都没有Gradle模块元数据,所以Gradle并不总是自动发现功能. 但是,使用规则声明组件功能以便在构建而不是运行时尽快发现冲突很有趣.

一个典型的例子是在新版本中,只要将组件重新放置在不同的坐标上. 例如,ASM库位于asm:asm坐标处,直到版本3.3.1 ,然后从4.0开始更改为org.ow2.asm:asm . 在类路径上同时具有ASM⇐3.3.1和4.0+是非法的,因为它们提供相同的功能,只是组件已被重定位. 因为每个组件都具有与其GAV坐标相对应的隐式功能,所以我们可以通过具有一条规则来"解决"此问题,该规则将声明asm:asm模块提供org.ow2.asm:asm功能:

例子1.按能力解决冲突
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)
                    }
                }
            }
        }
    }
}
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)
                }
            }
        }
    }
}

现在,只要在同一依赖图中找到两个组件,构建就将失败 .

在这个阶段,Gradle 只会使更多构建失败. 它不会自动为您解决问题,但是可以帮助您意识到自己有问题. 建议在插件中编写此类规则,然后将其应用于您的构建. 然后,用户必须尽可能表达自己的喜好,或者解决类路径上不兼容内容的问题,如以下部分所述.

Selecting between candidates

在某个时候,依赖图将包含不兼容的模块互斥的模块. 例如,您可能有不同的记录器实现,并且需要选择一个绑定. 功能可以帮助您意识到您有冲突,但是Gradle还提供了表达如何解决冲突的工具.

Selecting between different capability candidates

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

例子2.通过能力版本化解决冲突
build.gradle
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability('org.ow2.asm:asm') {
        selectHighestVersion()
    }
}
build.gradle.kts
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("org.ow2.asm:asm") {
        selectHighestVersion()
    }
}

但是,选择最高性能版本的冲突解决方法修复并不总是适合的. 例如,对于日志记录框架,我们使用什么版本的日志记录框架都没有关系,我们应该始终选择Slf4j.

在这种情况下,我们可以通过明确选择slf4j作为赢家来解决此问题:

例子3.用slf4j代替log4j
build.gradle
    configurations.all {
        resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
            select(candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module == 'log4j-over-slf4j' } )
            because 'use slf4j in place of log4j'
        }
    }
build.gradle.kts
    configurations.all {
        resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
            select(candidates.first { it.id.let { id -> id is ModuleComponentIdentifier && id.module == "log4j-over-slf4j" } } )
            because("use slf4j in place of log4j")
        }
    }

请注意,如果您在类路径上具有多个Slf4j绑定 ,则此方法也很好用:绑定本质上是不同的记录器实现,并且只需要一个. 但是,所选的实现可能取决于要解决的配置. 例如,对于测试来说, slf4j-simple可能就足够了,但是对于生产而言, slf4-over-log4j可能更好.

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