在依赖图中,通常会意外包含同一 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")
    }
}

在这种情况下,HikariCPcommons-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 功能

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)
                }
            }
        }
    }
}
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)
                    }
                }
            }
        }
    }
}

应用此规则后,如果在依赖图中同时存在 asm:asm (<= 3.3.1) 和 org.ow2.asm:asm (4.0+),构建将会失败。

Gradle 不会自动解决冲突,但这有助于您意识到问题的存在。建议将此类规则打包到插件中以用于构建,从而允许用户决定使用哪个版本或修复 classpath 冲突。

声明外部模块的功能(Capabilities)

Gradle 不仅允许您为您构建的组件声明功能,也允许您为未定义功能的外部组件声明功能。

例如,考虑您的构建文件中的以下依赖项

build.gradle.kts
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")
}
build.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'
}

就目前而言,这种设置会导致 classpath 中存在两个日志框架,这一点并不明显。具体来说,zookeeper 引入了 log4j,但我们想使用 log4j-over-slf4j

为了主动检测此冲突,我们可以定义一个规则,声明这两个框架提供相同的功能

build.gradle.kts
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)
                }
            }
        }
    }
}
build.gradle
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 的替代实现时,这非常有用

build.gradle.kts
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")
        }
    }
}
build.gradle
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)

在此示例中,我们声明了两个功能

  1. com.acme:my-library:1.0 - 库的隐式功能

  2. 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 的两个版本:“旧”模块和“迁移的”模块。我们可以通过自动选择具有最高功能版本的组件来解决冲突

build.gradle.kts
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("org.ow2.asm:asm") {
        selectHighestVersion()
    }
}
build.gradle
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability('org.ow2.asm:asm') {
        selectHighestVersion()
    }
}

然而,选择最高功能版本来解决冲突并非总是合适。

例如,对于日志框架,使用哪个版本的日志框架并不重要。在这种情况下,我们明确选择 slf4j 作为首选选项

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

如果您的 classpath 中有多个 slf4j 绑定,这种方法也同样有效;绑定本质上是不同的日志实现,您只需要一个。然而,所选的实现可能取决于正在解析的配置。

例如,在测试环境中,轻量级的 slf4j-simple 日志实现可能就足够了,而在生产环境中,像 logback 这样更健壮的解决方案可能更受欢迎。

只能选择依赖图中存在的模块进行解析。select 方法只接受当前候选集中的模块。如果所需的模块不是冲突的一部分,您可以选择不解析该特定冲突,从而使其保持未解析状态。图中的另一个冲突可能包含您想要选择的模块。

如果未对给定功能的所有冲突提供解析,构建将失败,因为为解析选择的模块未在图中找到。此外,调用 select(null) 将导致错误,应避免这样做。

有关更多信息,请参阅功能解析 API