在依赖图中,相同的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")
    }
}

在这种情况下,HikariCPcommons-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 能力的规则来解决此冲突

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 不会自动解决冲突,但这有助于您意识到问题的存在。建议将此类规则打包到插件中以用于构建,允许用户决定使用哪个版本或修复类路径冲突。

声明外部模块能力

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

就目前而言,这种设置导致类路径上有两个日志框架并不明显。具体来说,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 检测到冲突并以清晰的错误消息失败

dependencyReport.out
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 的替代实现时,这很有用

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

能力必须附加到传出配置,这些配置是组件的可消费配置

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

  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

在候选者之间选择

在某些时候,依赖图将包含不兼容的模块互斥的模块

例如,您可能拥有不同的日志实现,并且需要选择一个绑定。能力有助于理解冲突,然后 Gradle 为您提供解决冲突的工具。

在不同的能力候选者之间选择

在上面的重定位示例中,Gradle 能够告诉您类路径上有相同 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'
    }
}

如果类路径上有多个 slf4j 绑定,这种方法也很好用;绑定基本上是不同的日志实现,您只需要一个。但是,所选实现可能取决于要解析的配置。

例如,在测试环境中,轻量级的 slf4j-simple 日志实现可能就足够了,而在生产中,更强大的解决方案(如 logback)可能更可取。

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

如果未为给定能力上的所有冲突提供解决方案,则构建将失败,因为用于解决的模块未在图中找到。此外,调用 select(null) 将导致错误,应避免。

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