在依赖关系图中,常见的情况是意外包含了同一 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")
}
}
在这种情况下,HikariCP
和 commons-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
功能
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)
}
}
}
}
}
}
有了这个规则,如果依赖关系图中同时存在 asm:asm
( < = 3.3.1
) 和 org.ow2.asm:asm
(4.0+
),构建将会失败。
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
作为首选选项
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
日志记录器实现可能就足够了,而在生产环境中,可能更喜欢像 logback
这样更强大的解决方案。
只能选择依赖关系图中找到的模块进行解析。select
方法只接受当前候选者集合中的模块。如果所需的模块不是冲突的一部分,您可以选择不解决该特定冲突,实际上使其保持未解决状态。图中可能存在另一个冲突,其中包含您想要选择的模块。
如果没有为给定功能的所有冲突提供解决方案,构建将失败,因为为解析选择的模块未在图中找到。此外,调用 select(null)
将导致错误,应避免使用。
有关更多信息,请参阅 功能解析 API。