Gradle 的依赖管理引擎具有变体感知能力。

上一节中,Gradle 构建了一个已解析依赖项的图。在图解析期间,Gradle 根据构建要求选择了每个依赖项的适当变体

dependency resolution variant selection model

变体表示组件的不同使用方式,例如用于 Java 编译、原生链接或文档。每个变体可能都有自己的工件依赖项

Gradle 使用属性来确定选择哪个变体。这些属性为每个变体添加了上下文,描述了何时应该使用它们。

组件

让我们回顾一下上一节中对组件的描述。一个组件

  • 包含变体

    • 其中包含一个或多个工件

    • 其中包含零个或多个依赖项

    • 元数据描述

component model gradle

在上面的示例中,org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1组件。它有一个模块 org.jetbrains.kotlinx:kotlinx-serialization-json 和一个版本 1.5.1

component model gradle example

变体和属性

变体表示组件的不同版本或方面,例如 apiimplementationjarclasses

在上面的示例中,org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1 有四个变体jvmandroidjsnative

属性是类型安全的键值对,在变体选择期间由消费者生产者使用:属性 : 值

在上面的示例中,对于 org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1变体,有两个重要的属性

变体 属性 1 属性 2

JVM

org.gradle.usage: java-runtime

org.jetbrains.kotlin.platform.type: jvm

Android

org.gradle.usage: java-runtime

org.jetbrains.kotlin.platform.type: androidJvm

Javascript

org.gradle.usage: java-runtime

org.jetbrains.kotlin.platform.type: js

Native

org.gradle.usage: native-link

org.jetbrains.kotlin.platform.type: native

请注意,org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1元数据展示了更多用于描述其变体属性,例如 org.gradle.libraryelementsorg.gradle.category。但是,Gradle 可能会在依赖解析期间使用其中任何数量的属性,因为某些属性比其他属性更相关

"variants": [
...
  {
    "name": "jsLegacyRuntimeElements-published",
    "attributes": {
      "org.gradle.category": "library",
      "org.gradle.usage": "kotlin-runtime",
      "org.jetbrains.kotlin.js.compiler": "legacy",
      "org.jetbrains.kotlin.platform.type": "js"
    }
  },
  {
    "name": "jvmRuntimeElements-published",
    "attributes": {
      "org.gradle.category": "library",
      "org.gradle.libraryelements": "jar",
      "org.gradle.usage": "java-runtime",
      "org.jetbrains.kotlin.platform.type": "jvm"
    }
  },
...
]

Gradle 使用两种类型属性来将可用变体与构建所需的变体进行匹配

  • 消费者属性:定义可解析配置请求的变体的所需特性。

  • 生产者属性:每个变体都有一组描述其用途的属性

变体属性匹配

组件可以定义的变体数量没有限制。

典型的组件将至少包含一个实现变体,但也可能提供额外的变体,例如测试夹具、文档或源代码。此外,组件可以根据消费者为相同用途提供不同的变体。例如,在编译期间,组件可能为 Linux、Windows 和 macOS 提供不同的头文件。

Gradle 通过使用属性匹配算法,将消费者指定的属性生产者定义的属性进行匹配,从而执行变体感知选择

dependency resolution variant selection
变体名称主要用于调试和错误消息。它在变体匹配中不起作用;只有变体的属性用于匹配过程。

属性匹配算法

Gradle 的依赖解析引擎遵循此属性匹配算法来确定最佳候选者(如果找不到匹配项则失败)

dep man adv 5

步骤 1:查找兼容候选者

请求的属性候选属性进行匹配。

  • 对于每个候选者

    • 对于每个候选属性

      • 如果候选属性缺失,则将该候选者标记为缺失。

      • 否则,如果候选属性与请求的属性完全匹配,则该候选者保持兼容。

      • 否则,如果候选属性通过兼容性测试,则该候选者保持兼容。

      • 否则,该候选者被淘汰。

步骤 2:最多匹配属性

过滤后,Gradle 检查一个候选者是否是所有其他候选者的严格超集

  • 如果只有一个候选者剩余,则该候选者胜出。

  • 如果有多个候选者剩余,请检查其中一个

    • 匹配更多属性

    • 与另一个候选者相比没有缺失属性

  • 如果存在这样的严格超集,则该候选者胜出。

步骤 3:使用请求属性进行消歧

如果仍有多个候选者,Gradle 将尝试消歧

  • 请求的属性按优先级排序

  • 对于每个按优先级顺序请求属性

    • 如果存在消歧规则,则应用它。

    • 如果所有剩余候选者中某个请求属性的值相同,则忽略它。

  • 如果只有一个候选者剩余,则该候选者胜出。

如果仍有多个候选者剩余,Gradle 将处理无序属性(不在优先级列表中的属性)

  • 对于每个没有优先级顺序请求属性

    • 如果存在消歧规则,则应用它。

    • 如果所有剩余候选者中某个请求属性的值相同,则忽略它。

  • 如果只有一个候选者剩余,则该候选者胜出。

  • 如果没有候选者剩余,Gradle 将回退到最兼容的候选者(步骤 2)。

步骤 4:使用额外属性进行消歧

如果仍有多个候选者,Gradle 将考虑额外属性

  • 这些属性仅存在于某些候选者中

  • 缺少额外属性的候选者更受青睐。

  • 考虑属性优先级顺序

首先,Gradle 处理优先级额外属性

  • 对于按优先级顺序的每个额外属性

    • 如果某些候选者具有该属性而另一些候选者没有

      • 缺少额外属性的候选者更受青睐。

      • 具有额外属性的候选者被淘汰。

  • 如果只有一个候选者剩余,则该候选者胜出。

  • 如果没有候选者剩余,Gradle 将回退到原始兼容候选者(步骤 1)。

然后,Gradle 处理无序额外属性

  • 对于每个剩余的没有定义优先级额外属性

    • 如果该属性仅存在于某些候选者中

      • 不带该属性的候选者更受青睐。

      • 带有额外属性的候选者被淘汰。

  • 如果只有一个候选者剩余,则该候选者胜出。

  • 如果没有候选者剩余,Gradle 将回退到原始兼容候选者(步骤 1)。

步骤 5:使用额外属性的存在性进行消歧

如果仍然有候选者,Gradle 会根据请求属性淘汰候选者

  • 对于每个额外属性

    • 如果所有候选者提供相同的值,则忽略它。

    • 否则,删除提供该属性的候选者

  • 如果只有一个候选者剩余,则该候选者胜出。

步骤 6:失败条件

除非另有说明,如果任何时候没有候选者剩余,则解析失败

此外,Gradle 会输出一个包含步骤 1 中所有兼容候选者的列表,以帮助调试属性匹配失败。

插件和生态系统可以通过实现兼容性规则、消歧规则以及定义属性的优先级来影响选择算法。优先级较高的属性将按顺序用于淘汰候选者。

例如,在 Java 生态系统中,org.gradle.usage 属性的优先级高于 org.gradle.libraryelements。这意味着如果两个候选者都具有与 org.gradle.usageorg.gradle.libraryelements 兼容的值,Gradle 将选择通过 org.gradle.usage 消歧规则的候选者。

变体感知解析过程有两个例外

  • 当生产者没有变体时,将选择默认工件

  • 当消费者按名称显式选择配置时,将使用与该配置关联的工件

一个简单示例

让我们看一个消费者尝试使用库进行编译的示例。

首先,消费者详细说明了它将如何使用依赖解析的结果。这是通过在消费者的可解析配置上设置属性来实现的。

在这种情况下,消费者想要解析一个匹配 org.gradle.usage=java-api变体

接下来,生产者暴露其组件的不同变体

  • API 变体(名为 apiElements),带有属性 org.gradle.usage=java-api

  • 运行时变体(名为 runtimeElements),带有属性 org.gradle.usage=java-runtime

最后,Gradle 评估变体并选择正确的变体

  • 消费者请求一个带有属性 org.gradle.usage=java-api变体

  • 生产者apiElements 变体匹配此请求。

  • 生产者runtimeElements 变体不匹配。

消费者请求属性 生产者可用属性 生产者变体 匹配?

org.gradle.usage=java-api

org.gradle.usage=java-api

apiElements

✅ 是

org.gradle.usage=java-api

org.gradle.usage=java-runtime

runtimeElements

❌ 否

结果,Gradle 选择了 apiElements 变体,并将其工件依赖项提供给消费者

一个复杂示例

在实际场景中,消费者生产者通常都使用多个属性

例如,Gradle 中的 Java 库项目将涉及多个属性

属性 描述

org.gradle.usage

描述变体的使用方式。

org.gradle.dependency.bundling

描述变体如何处理依赖项(例如,shadow jar、fat jar、regular jar)。

org.gradle.libraryelements

描述变体的打包方式(例如,classes 或 jar)。

org.gradle.jvm.version

描述变体所针对的 Java 最低版本

org.gradle.jvm.environment

描述变体所针对的 JVM 类型。

让我们考虑一个场景,消费者想要使用 Java 8 上的库运行测试,而生产者支持两个版本:Java 8 和 Java 11。

步骤 1:消费者指定要求。

消费者想要解析一个变体,它

  • 可以在运行时使用(org.gradle.usage=java-runtime)。

  • 至少可以在 Java 8 上运行(org.gradle.jvm.version=8)。

步骤 2:生产者公开多个变体。

生产者为 Java 8 和 Java 11 提供变体,用于 API 和运行时用途

变体名称 属性

apiJava8Elements

org.gradle.usage=java-api, org.gradle.jvm.version=8

runtime8Elements

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

apiJava11Elements

org.gradle.usage=java-api, org.gradle.jvm.version=11

runtime11Elements

org.gradle.usage=java-runtime, org.gradle.jvm.version=11

步骤 3:Gradle 匹配属性。

Gradle 比较消费者请求的属性生产者变体

  • 消费者请求一个带有 org.gradle.usage=java-runtimeorg.gradle.jvm.version=8变体

  • runtime8Elementsruntime11Elements 都匹配 org.gradle.usage=java-runtime 属性

  • API 变体apiJava8ElementsapiJava11Elements)被丢弃,因为它们不匹配 org.gradle.usage=java-runtime

  • 选择变体 runtime8Elements,因为它与 Java 8 兼容。

  • 变体 runtime11Elements 不兼容,因为它需要 Java 11。

消费者请求属性 生产者可用属性 生产者变体 匹配?

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

runtime8Elements

✅ 已选择

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

org.gradle.usage=java-runtime, org.gradle.jvm.version=11

runtime11Elements

❌ 不兼容

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

org.gradle.usage=java-api, org.gradle.jvm.version=8

apiJava8Elements

❌ 已丢弃

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

org.gradle.usage=java-api, org.gradle.jvm.version=11

apiJava11Elements

❌ 已丢弃

Gradle 选择 runtime8Elements 并将其工件依赖项提供给消费者

但是如果消费者org.gradle.jvm.version 设置为 7 会发生什么?

在这种情况下,依赖解析将失败,并伴随一个错误,解释没有合适的变体。Gradle 知道消费者需要一个 Java 7 兼容的库,但生产者的最低版本是 8。

如果消费者请求 org.gradle.jvm.version=15,Gradle 可以选择 Java 8 或 Java 11 变体。Gradle 会选择最高兼容版本——Java 11。

可视化变体信息

Gradle 提供了内置任务来可视化变体选择过程,并显示所涉及的生产者消费者属性

传出变体报告

报告任务 outgoingVariants 显示了项目消费者可供选择的变体列表。它显示了每个变体功能属性工件

此任务类似于 dependencyInsight 报告任务

默认情况下,outgoingVariants 打印所有变体的信息。它提供可选参数 --variant 来选择单个变体进行显示。它还接受 --all 标志以包含旧版和已弃用配置的信息,或 --no-all 以排除此信息。

以下是新生成的 java-library 项目上 outgoingVariants 任务的输出

> Task :outgoingVariants
--------------------------------------------------
Variant apiElements
--------------------------------------------------
API elements for the 'main' feature.

Capabilities
    - new-java-library:lib:unspecified (default capability)
Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.version         = 11
    - org.gradle.libraryelements     = jar
    - org.gradle.usage               = java-api
Artifacts
    - build/libs/lib.jar (artifactType = jar)

Secondary Variants (*)

    --------------------------------------------------
    Secondary Variant classes
    --------------------------------------------------
        Description = Directories containing compiled class files for main.

        Attributes
            - org.gradle.category            = library
            - org.gradle.dependency.bundling = external
            - org.gradle.jvm.version         = 11
            - org.gradle.libraryelements     = classes
            - org.gradle.usage               = java-api
        Artifacts
            - build/classes/java/main (artifactType = java-classes-directory)

--------------------------------------------------
Variant mainSourceElements (i)
--------------------------------------------------
Description = List of source directories contained in the Main SourceSet.

Capabilities
    - new-java-library:lib:unspecified (default capability)
Attributes
    - org.gradle.category            = verification
    - org.gradle.dependency.bundling = external
    - org.gradle.verificationtype    = main-sources
Artifacts
    - src/main/java (artifactType = directory)
    - src/main/resources (artifactType = directory)

--------------------------------------------------
Variant runtimeElements
--------------------------------------------------
Runtime elements for the 'main' feature.

Capabilities
    - new-java-library:lib:unspecified (default capability)
Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.version         = 11
    - org.gradle.libraryelements     = jar
    - org.gradle.usage               = java-runtime
Artifacts
    - build/libs/lib.jar (artifactType = jar)

Secondary Variants (*)

    --------------------------------------------------
    Secondary Variant classes
    --------------------------------------------------
        Description = Directories containing compiled class files for main.

        Attributes
            - org.gradle.category            = library
            - org.gradle.dependency.bundling = external
            - org.gradle.jvm.version         = 11
            - org.gradle.libraryelements     = classes
            - org.gradle.usage               = java-runtime
        Artifacts
            - build/classes/java/main (artifactType = java-classes-directory)

    --------------------------------------------------
    Secondary Variant resources
    --------------------------------------------------
        Description = Directories containing the project's assembled resource files for use at runtime.

        Attributes
            - org.gradle.category            = library
            - org.gradle.dependency.bundling = external
            - org.gradle.jvm.version         = 11
            - org.gradle.libraryelements     = resources
            - org.gradle.usage               = java-runtime
        Artifacts
            - build/resources/main (artifactType = java-resources-directory)

从中可以看出 Java 库公开的两个主要变体apiElementsruntimeElements。请注意,主要区别在于 org.gradle.usage 属性,其值为 java-apijava-runtime。正如它们所指出的,这就是在消费者编译类路径上需要什么与在运行时类路径上需要什么之间的区别。

它还显示了次要变体,这些变体是 Gradle 项目独有的,未发布。例如,apiElements 中的次要变体 classes 允许 Gradle 在针对java-library 项目编译时跳过 JAR 创建。

关于无效可消费配置的信息

一个项目不能有多个具有相同属性功能的配置。在这种情况下,项目将无法构建。

为了能够可视化这些问题,传出变体报告以宽松的方式处理这些错误。这允许报告显示有关问题的信息。

可解析配置报告

Gradle 还提供了一个补充报告任务 resolvableConfigurations,它显示项目的可解析配置,即那些可以添加依赖项并被解析的配置。

该报告将列出它们的属性以及它们扩展的任何配置。它还将列出在解析期间将受兼容性规则消歧规则影响的任何属性的摘要。

默认情况下,resolvableConfigurations 打印所有纯可解析配置的信息。这些配置被标记为可解析但标记为可消费。尽管某些可解析配置也被标记为可消费,但这些是旧版配置,不应在构建脚本中添加依赖项。

本报告提供

  • 一个可选参数

    • --configuration → 选择要显示的单个配置。

  • 用于包含或排除旧版和已弃用配置的标志

    • --all → 包含有关旧版和已弃用配置的信息。

    • --no-all → 排除此信息。

  • 用于控制扩展配置部分中的传递扩展的标志

    • --recursive → 列出传递而不是直接扩展的配置。

    • --no-recursive → 排除此信息。

以下是新生成的 java-library 项目上 resolvableConfigurations 任务的输出

> Task :resolvableConfigurations
--------------------------------------------------
Configuration compileClasspath
--------------------------------------------------
Description = Compile classpath for source set 'main'.

Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.environment     = standard-jvm
    - org.gradle.jvm.version         = 11
    - org.gradle.libraryelements     = classes
    - org.gradle.usage               = java-api
Extended Configurations
    - compileOnly
    - implementation

--------------------------------------------------
Configuration runtimeClasspath
--------------------------------------------------
Description = Runtime classpath of source set 'main'.

Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.environment     = standard-jvm
    - org.gradle.jvm.version         = 11
    - org.gradle.libraryelements     = jar
    - org.gradle.usage               = java-runtime
Extended Configurations
    - implementation
    - runtimeOnly

[...]

从中可以看出用于解析依赖项的两个主要配置:compileClasspathruntimeClasspath,以及它们相应的测试配置(已截断)。

现在我们了解了变体选择和属性匹配,让我们继续进行依赖解析的工件解析阶段。这个阶段也具有变体感知能力。

下一步: 了解工件解析 >>