组件能力介绍
通常,依赖关系图会意外地包含同一 API 的多个实现。这在日志记录框架中尤其常见,其中有多个绑定可用,并且当另一个传递依赖项选择另一个绑定时,一个库会选择一个绑定。由于这些实现位于不同的 GAV 坐标,因此构建工具通常无法发现这些库之间存在冲突。为了解决这个问题,Gradle 提供了能力的概念。
在单个依赖图中找到两个提供相同功能的组件是非法的。直观上,这意味着如果 Gradle 发现两个在类路径上提供相同内容的组件,它将失败并显示错误,指示哪些模块存在冲突。在我们的示例中,这意味着日志记录框架的不同绑定提供相同的功能。
能力坐标
能力由三元组定义。(group, module, version)
每个组件定义了与其 GAV 坐标(组、工件、版本)相对应的隐式功能。例如,该模块具有包含 group 、 name和 version 的org.apache.commons:commons-lang3:3.8
隐式功能。重要的是要认识到功能是有版本的。org.apache.commons
commons-lang3
3.8
声明组件功能
默认情况下,如果依赖图中的两个组件提供相同的功能,Gradle 将失败。由于大多数模块目前在没有 Gradle 模块元数据的情况下发布,因此 Gradle 并不总是自动发现功能。然而,使用规则来声明组件功能以便在构建期间而不是运行时尽快发现冲突是很有趣的。
一个典型的例子是每当一个组件在新版本中重新定位到不同的坐标时。例如,ASM 库在asm:asm
version 之前的坐标为3.3.1
,然后更改为org.ow2.asm:asm
since 4.0
。在类路径上同时存在 ASM <= 3.3.1 和 4.0+ 是非法的,因为它们提供相同的功能,只是组件已重新定位。因为每个组件都有与其 GAV 坐标相对应的隐式功能,所以我们可以通过制定一条声明该asm:asm
模块提供该org.ow2.asm:asm
功能的规则来“修复”此问题:
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)
}
}
}
}
}
@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 的两个版本:一个“旧”模块和一个“重定位”模块。现在我们可以通过自动选择具有最高能力版本的组件来解决冲突:
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability("org.ow2.asm:asm") {
selectHighestVersion()
}
}
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability('org.ow2.asm:asm') {
selectHighestVersion()
}
}
然而,通过选择最高功能版本冲突解决方案来修复并不总是合适的。例如,对于日志框架,无论我们使用什么版本的日志框架,我们都应该始终选择 Slf4j。
在这种情况下,我们可以通过明确选择 slf4j 作为获胜者来修复它:
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")
}
}
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。