在依赖图中,相同的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 允许您不仅为构建的组件声明能力,还为未定义能力的外部组件声明能力。
例如,考虑构建文件中的以下依赖项
dependencies {
// This dependency will bring log4:log4j transitively
implementation("org.apache.zookeeper:zookeeper:3.4.9")
// We use log4j over slf4j
implementation("org.slf4j:log4j-over-slf4j:1.7.10")
}
dependencies {
// This dependency will bring log4:log4j transitively
implementation 'org.apache.zookeeper:zookeeper:3.4.9'
// We use log4j over slf4j
implementation 'org.slf4j:log4j-over-slf4j:1.7.10'
}
就目前而言,这种设置导致类路径上有两个日志框架并不明显。具体来说,zookeeper
引入了 log4j
,但我们希望使用 log4j-over-slf4j
。
为了主动检测此冲突,我们可以定义一个规则,声明这两个框架提供相同的能力
dependencies {
// Activate the "LoggingCapability" rule
components.all(LoggingCapability::class.java)
}
class LoggingCapability : ComponentMetadataRule {
val loggingModules = setOf("log4j", "log4j-over-slf4j")
override
fun execute(context: ComponentMetadataContext) = context.details.run {
if (loggingModules.contains(id.name)) {
allVariants {
withCapabilities {
// Declare that both log4j and log4j-over-slf4j provide the same capability
addCapability("log4j", "log4j", id.version)
}
}
}
}
}
dependencies {
// Activate the "LoggingCapability" rule
components.all(LoggingCapability)
}
@CompileStatic
class LoggingCapability implements ComponentMetadataRule {
final static Set<String> LOGGING_MODULES = ["log4j", "log4j-over-slf4j"] as Set<String>
void execute(ComponentMetadataContext context) {
context.details.with {
if (LOGGING_MODULES.contains(id.name)) {
allVariants {
it.withCapabilities {
// Declare that both log4j and log4j-over-slf4j provide the same capability
it.addCapability("log4j", "log4j", id.version)
}
}
}
}
}
}
这确保 Gradle 检测到冲突并以清晰的错误消息失败
log4j:log4j:1.2.16 FAILED
Failures:
- Could not resolve log4j:log4j:1.2.16.
- Module 'log4j:log4j' has been rejected:
Cannot select module with conflict on capability 'log4j:log4j:1.2.16' also provided by [org.slf4j:log4j-over-slf4j:1.7.10(compile)]
log4j:log4j:1.2.16 FAILED
\--- org.apache.zookeeper:zookeeper:3.4.9
\--- compileClasspath
org.slf4j:log4j-over-slf4j:1.7.10 FAILED
Failures:
- Could not resolve org.slf4j:log4j-over-slf4j:1.7.10.
- Module 'org.slf4j:log4j-over-slf4j' has been rejected:
Cannot select module with conflict on capability 'log4j:log4j:1.7.10' also provided by [log4j:log4j:1.2.16(compile)]
org.slf4j:log4j-over-slf4j:1.7.10 FAILED
\--- compileClasspath
声明本地组件能力
每个组件都有一个与其 GAV 坐标匹配的隐式能力。但是,您还可以声明额外的显式能力,当以不同 GAV 坐标发布的库充当相同 API 的替代实现时,这很有用
configurations {
apiElements {
outgoing {
capability("com.acme:my-library:1.0")
capability("com.other:module:1.1")
}
}
runtimeElements {
outgoing {
capability("com.acme:my-library:1.0")
capability("com.other:module:1.1")
}
}
}
configurations {
apiElements {
outgoing {
capability("com.acme:my-library:1.0")
capability("com.other:module:1.1")
}
}
runtimeElements {
outgoing {
capability("com.acme:my-library:1.0")
capability("com.other:module:1.1")
}
}
}
能力必须附加到传出配置,这些配置是组件的可消费配置。
在此示例中,我们声明了两个能力
-
com.acme:my-library:1.0
- 库的隐式能力。 -
com.other:module:1.1
- 分配给此库的附加能力。
重要的是显式声明隐式能力,因为一旦定义了任何显式能力,所有能力都必须声明,包括隐式能力。
第二个能力可以是此库特有的,也可以与外部组件提供的能力匹配。如果 com.other:module
出现在依赖图中的其他位置,构建将失败,并且消费者必须选择使用哪个模块。
能力发布在 Gradle Module Metadata 中,但在 POM 或 Ivy 元数据文件中没有等效项。因此,当发布此类组件时,Gradle 警告此功能仅支持 Gradle 消费者
Maven publication 'maven' contains dependencies that cannot be represented in a published pom file. - Declares capability com.acme:my-library:1.0 - Declares capability com.other:module:1.1
在候选者之间选择
在某些时候,依赖图将包含不兼容的模块或互斥的模块。
例如,您可能拥有不同的日志实现,并且需要选择一个绑定。能力有助于理解冲突,然后 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。