功能
在依赖图中,通常会意外包含同一 API 的多个实现,尤其是对于日志框架等库,不同的传递性依赖项可能会选择不同的绑定。
由于这些实现通常位于不同的 Group、Artifact 和 Version (GAV) 坐标下,构建工具通常无法检测到冲突。
为了解决这个问题,Gradle 引入了功能(capability)的概念。
理解功能(Capabilities)
功能本质上是一种声明不同组件(依赖项)提供相同功能的方式。
在单个依赖图中,Gradle 非法包含提供相同功能的多个组件。如果 Gradle 检测到两个组件提供相同功能(例如,日志框架的不同绑定),它将通过错误使构建失败,并指示冲突的模块。这确保了冲突的实现得到解决,避免了 classpath 上的问题。
例如,假设您依赖了两个不同的数据库连接池库
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
都提供了相同的功能(连接池)。如果两者都存在于 classpath 中,Gradle 将会失败。
由于只能使用其中一个,Gradle 的解析策略允许您选择 HikariCP
,从而解决冲突。
理解功能坐标(Capability Coordinates)
一个功能(capability)由一个 (group, module, version)
三元组标识。
每个组件都根据其 GAV 坐标定义了一个隐式功能:group、artifact 和 version。
例如,org.apache.commons:commons-lang3:3.8
模块有一个隐式功能,其 group 为 org.apache.commons
,name 为 commons-lang3
,version 为 3.8
dependencies {
implementation("org.apache.commons:commons-lang3:3.8")
}
需要注意的是,功能是有版本的。
声明组件功能(Component Capabilities)
为了尽早检测冲突,通过规则声明组件功能(component capabilities)非常有用,这使得冲突可以在构建期间而非运行时被捕获。
一种常见场景是组件在新版本中被迁移到不同的坐标。
例如,ASM 库在版本 3.3.1
之前以 asm:asm
发布,然后从版本 4.0
开始迁移到 org.ow2.asm:asm
。在 classpath 中包含这两个版本是非法的,因为它们在不同坐标下提供了相同的功能。
由于每个组件都有基于其 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 不会自动解决冲突,但这有助于您意识到问题的存在。建议将此类规则打包到插件中以用于构建,从而允许用户决定使用哪个版本或修复 classpath 冲突。 |
声明外部模块的功能(Capabilities)
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'
}
就目前而言,这种设置会导致 classpath 中存在两个日志框架,这一点并不明显。具体来说,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 检测到冲突并以清晰的错误消息失败
> Could not resolve all files for configuration ':compileClasspath'. > Could not resolve org.slf4j:log4j-over-slf4j:1.7.10. Required by: project : > 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)] > Could not resolve log4j:log4j:1.2.16. Required by: project : > org.apache.zookeeper:zookeeper:3.4.9 > 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)]
声明本地组件的功能(Capabilities)
每个组件都有一个匹配其 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")
}
}
}
功能必须附加到出站配置(outgoing configurations),它们是组件的可消费配置(consumable configurations)。
在此示例中,我们声明了两个功能
-
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
在候选者之间进行选择
在某个时刻,依赖图将包含不兼容的模块或互斥的模块。
例如,您可能有不同的日志实现,需要选择一个绑定。功能(Capabilities)帮助您理解冲突,然后 Gradle 提供工具来解决冲突。
在不同的功能候选者之间进行选择
在上面的迁移示例中,Gradle 可以告诉您 classpath 中存在同一 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'
}
}
如果您的 classpath 中有多个 slf4j
绑定,这种方法也同样有效;绑定本质上是不同的日志实现,您只需要一个。然而,所选的实现可能取决于正在解析的配置。
例如,在测试环境中,轻量级的 slf4j-simple
日志实现可能就足够了,而在生产环境中,像 logback
这样更健壮的解决方案可能更受欢迎。
只能选择依赖图中存在的模块进行解析。select
方法只接受当前候选集中的模块。如果所需的模块不是冲突的一部分,您可以选择不解析该特定冲突,从而使其保持未解析状态。图中的另一个冲突可能包含您想要选择的模块。
如果未对给定功能的所有冲突提供解析,构建将失败,因为为解析选择的模块未在图中找到。此外,调用 select(null)
将导致错误,应避免这样做。
有关更多信息,请参阅功能解析 API。