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

In the relocation example above, Gradle was able to tell you that you have two versions of the same API on classpath: an "old" module and a "relocated" one. 在上面的重定位示例中,Gradle能够告诉您在类路径上有两个版本的同一API:一个"旧"模块和一个"重定位"模块. Now we can solve the conflict by automatically choosing the component which has the highest capability version: 现在,我们可以通过自动选择功能最高的组件来解决冲突:

例子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") {
            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'
        }
    }
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")
        }
    }

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

只能使用图中找到的模块来进行分辨率.

select方法仅接受当前候选中找到的模块. 如果您要选择的模块不是冲突的一部分,则可以放弃选择,从而无法有效解决冲突. 对于相同的功能,图中可能存在另一个冲突,并且将具有您要选择的模块.

如果没有针对给定功能上的所有冲突给出解决方案,则由于为解决方案选择的模块根本不是图形的一部分,构建将失败.

另外select(null)将导致错误,因此应避免使用.

For more information, check out the the capabilities resolution API.