依赖图解析阶段,Gradle 会构建一个已解析的依赖图,它建模了不同组件及其变体之间的关系。

模块

依赖图解析始于构建脚本中声明的依赖

build.gradle.kts
dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
}

学习基础知识章节中,我们了解到模块是一个已发布的工作单元(例如库或应用),而依赖是对项目编译或运行所需的模块的引用。

在上面的示例中,模块com.fasterxml.jackson.core:jackson-databind

组件

模块的每个版本被称为一个组件

在上面关于com.fasterxml.jackson.core:jackson-databind:2.17.2模块的示例中,2.17.2 是版本。

元数据

组件元数据详细描述,元数据可在托管该组件的仓库中获取,格式为 ivypomGMM 元数据。

这是com.fasterxml.jackson.core:jackson-databind:2.17.2元数据的精简示例

jackson-databind-2.17.2.module
{
  "formatVersion": "1.1",
  "component": {
    "group": "com.fasterxml.jackson.core",
    "module": "jackson-databind",
    "version": "2.17.2"
  },
  "variants": [
    {
      "name": "apiElements",
      ...
    },
    {
      "name": "runtimeElements",
      "attributes": {
        "org.gradle.category": "library",
        "org.gradle.dependency.bundling": "external",
        "org.gradle.libraryelements": "jar",
        "org.gradle.usage": "java-runtime"
      },
      "dependencies": [
        {
          "group": "com.fasterxml.jackson.core",
          "module": "jackson-annotations",
          "version": {
            "requires": "2.17.2"
          }
        }
      ],
      "files": [
        {
          "name": "jackson-databind-2.17.2.jar"
        }
      ]
    }
  ]
}

文件中的一些项对您来说应该很熟悉,例如 "files"、"dependencies"、"components"、"modulee" 和 "version"。让我们重点关注元数据中提供的变体

变体

变体是针对特定用例或环境定制的组件的特定变种。

变体允许您根据组件的使用上下文提供不同的定义。

如上所述,com.fasterxml.jackson.core:jackson-databind:2.17.2组件元数据提供了两个变体

  • apiElements变体包含项目针对 Jackson Databind 进行编译所需的依赖。

  • runtimeElements变体包含在运行时执行 Jackson Databind 所需的依赖。

变体 依赖 制品

apiElements

com.fasterxml.jackson.core, com.fasterxml.jackson.bom

jackson-databind-2.17.2.jar

runtimeElements

com.fasterxml.jackson.core

jackson-databind-2.17.2.jar

…​ 其他变体 …​

…​ 一些依赖 …​

…​ 一些制品 …​

每个变体都包含一组制品并定义了一组依赖(即被视为构建的传递性依赖)

  • com.fasterxml.jackson.core:jackson-databind:2.17.2runtimeElements变体

    • 依赖于 com.fasterxml.jackson.core

    • 提供一个名为 jackson-databind-2.17.2.jar 的制品。

为了区分 apiElementsruntimeElements 变体,Gradle 使用属性

属性

为了区分变体,Gradle 使用属性

属性用于定义变体的特定特征或属性,以及应使用这些变体的上下文。

在 Jackson Databind 的元数据中,我们看到 runtimeElements变体org.gradle.categoryorg.gradle.dependency.bundlingorg.gradle.libraryelementorg.gradle.usage 属性描述

{
  "variants": [
    {
      "name": "runtimeElements",
      "attributes": {
        "org.gradle.category": "library",
        "org.gradle.dependency.bundling": "external",
        "org.gradle.libraryelements": "jar",
        "org.gradle.usage": "java-runtime"
      }
    }
  ]
}

属性被定义为 key:value 对,例如 org.gradle.category": "library"

现在我们理解了依赖管理的构建块,接下来让我们看看依赖图解析。

依赖图

Gradle 构建一个依赖图,表示配置的依赖及其关系。此图包含直接和传递性依赖

图由节点组成,每个节点表示一个变体。这些节点通过边连接,代表变体之间的依赖关系。边表示一个变体如何依赖于另一个。

dependencies 任务可用于部分可视化依赖图的结构

$ ./gradlew app:dependencies

[...]

runtimeClasspath - Runtime classpath of source set 'main'.
\--- com.fasterxml.jackson.core:jackson-databind:2.17.2
     +--- com.fasterxml.jackson.core:jackson-annotations:2.17.2
     |    \--- com.fasterxml.jackson:jackson-bom:2.17.2
     |         +--- com.fasterxml.jackson.core:jackson-annotations:2.17.2
     |         +--- com.fasterxml.jackson.core:jackson-core:2.17.2
     |         \--- com.fasterxml.jackson.core:jackson-databind:2.17.2
     +--- com.fasterxml.jackson.core:jackson-core:2.17.2
     |    \--- com.fasterxml.jackson:jackson-bom:2.17.2
     \--- com.fasterxml.jackson:jackson-bom:2.17.2

在此截断的输出中,runtimeClasspath 代表项目中的特定可解析配置。

每个可解析配置计算单独的依赖图。 这是因为对于同一组声明的依赖,不同的配置可以解析到不同的传递性依赖集。

在上面的示例中,可解析配置 compileClasspath 可以解析一组不同的依赖,并生成与 runtimeClasspath 非常不同的图。

那么 Gradle 是如何构建依赖图的呢?

依赖图解析流程

依赖图解析以节点逐个(即,变体逐个)的方式操作。

循环的每次迭代一次处理一个节点,从将一个节点出队列开始。

最初,队列是空的。当进程启动时,一个根节点被添加到队列中。根节点实际上就是可解析配置

dep man adv 2

Gradle 通过从队列中拉出根节点来开始循环。Gradle 检查根节点依赖,解决它们的冲突,并下载它们的元数据。根据它们的元数据,Gradle 选择这些依赖的变体并将其添加回队列中。

变体的依赖对应于可解析配置声明的依赖

此时,队列包含根节点依赖的所有选定变体,这些变体现在将逐个处理。

对于循环中的每个节点,Gradle 会:

  1. 评估其依赖

  2. 使用冲突解决确定其目标版本。

  3. 一次性下载所有组件元数据

  4. 为每个组件选择变体

  5. 变体添加到顶级队列。

循环重复进行,直到节点队列为空。一旦过程完成,依赖图就已解析

依赖图解析在并行元数据下载和单线程逻辑之间交替进行,对于单个图重复此模式。

冲突解决

在执行依赖解析时,Gradle 处理两种类型的冲突:

  1. 版本冲突:当多个依赖请求相同的依赖但版本不同时发生。Gradle 必须选择将哪个版本包含在图中。

  2. 实现/功能冲突:当依赖图包含提供相同功能或功能的不同模块时发生。Gradle 通过选择一个模块来避免重复实现来解决这些冲突。

依赖解析过程高度可定制,许多 API 可以影响此过程。

A. 版本冲突

当两个组件出现以下情况时,可能会发生版本冲突:

  • 依赖于同一个模块,例如 com.google.guava:guava

  • 但版本不同,例如 20.025.1-android

    • 我们的项目直接依赖于 com.google.guava:guava:20.0

    • 我们的项目还依赖于 com.google.inject:guice:4.2.2,而后者又依赖于 com.google.guava:guava:25.1-android

Gradle 必须通过选择一个版本包含在依赖图中来解决此冲突。

Gradle 会考虑依赖图中的所有请求版本,默认选择最高版本。详细的版本排序在版本排序中解释。

Gradle 还支持富版本声明的概念,这意味着什么构成“最高”版本取决于版本的声明方式:

  • 无范围:将选择最高的非拒绝版本。

    • 如果声明的 strictly 版本低于最高版本,解析将失败。

  • 有范围::

    • 如果非范围版本符合范围或高于上限,则会选择该版本。

    • 如果只存在范围,选择取决于这些范围的交集:

      • 如果范围重叠,将选择交集中最高的现有版本。

      • 如果不存在明确的交集,将选择最大范围中的最高版本。如果最高范围中不存在版本,解析将失败。

    • 如果声明的 strictly 版本低于最高版本,解析将失败。

对于版本范围,Gradle 需要执行中间元数据查找以确定有哪些变种可用,这在元数据检索中解释。

带限定符的版本

“限定符”一词指的是版本字符串中非点分隔符(如连字符或下划线)后面的部分。

例如:

原始版本 基本版本 限定符

1.2.3

1.2.3

<无>

1.2-3

1.2

3

1_alpha

1

alpha

abc

abc

<无>

1.2b3

1.2

b3

abc.1+3

abc.1

3

b1-2-3.3

b

1-2-3.3

正如您所见,分隔符可以是 .-_+ 字符中的任意一个,加上版本中数字部分和非数字部分相邻时的空字符串。

默认情况下,Gradle 在解决冲突时优先选择不带限定符的版本。

例如,在版本 1.0-beta 中,基本形式是 1.0beta 是限定符。不带限定符的版本被认为更稳定,因此 Gradle 会优先选择它们。

这里有一些示例进行说明:

  • 1.0.0 (无限定符)

  • 1.0.0-beta (限定符: beta)

  • 2.1-rc1 (限定符: rc1)

即使限定符的字典序更高,Gradle 通常也会认为 1.0.0 这样的版本高于 1.0.0-beta

在解决版本冲突时,Gradle 应用以下逻辑:

  1. 基本版本比较: Gradle 首先选择具有最高基本版本(忽略任何限定符)的版本。所有其他版本都被丢弃。

  2. 限定符处理: 如果仍然存在具有相同基本版本的多个版本,Gradle 会优先选择不带限定符的版本(即发布版本)。如果所有版本都带有限定符,Gradle 会考虑限定符的顺序,优先选择更稳定的(如 "release"),而不是 "beta" 或 "alpha" 等。

B. 实现/功能冲突

在以下场景中会发生冲突

  • 不兼容变体:当两个模块尝试选择依赖的不同且不兼容的变体时。

  • 相同功能:当多个模块声明相同的功能,造成功能上的重叠时。

此类冲突在下面描述的变体选择期间解决。

元数据检索

Gradle 在依赖图中需要模块元数据有两个原因:

  1. 确定动态依赖的现有版本:当指定了动态版本(如 1.+latest.release)时,Gradle 必须识别可用的具体版本。

  2. 解析特定版本的模块依赖:Gradle 根据指定版本检索与模块关联的依赖,确保将正确的传递性依赖包含在构建中。

A. 确定动态依赖的现有版本

当遇到动态版本时,Gradle 必须通过以下步骤识别可用的具体版本:

  1. 检查仓库:Gradle 按照添加顺序检查每个定义的仓库。它不会在第一个返回元数据的仓库处停止,而是继续检查所有可用仓库。

  2. Maven 仓库:Gradle 从 maven-metadata.xml 文件检索版本信息,该文件列出了可用版本。

  3. Ivy 仓库:Gradle 借助目录列表收集可用版本。

结果是一个 Gradle 评估并与动态版本匹配的候选版本列表。Gradle 缓存此信息以优化未来的解析。此时,版本冲突解决将恢复。

B. 解析特定版本的模块依赖

当 Gradle 尝试解析特定版本的所需依赖时,它遵循此过程:

  1. 仓库检查:Gradle 按照定义的顺序检查每个仓库。

    • 它查找描述模块的元数据文件(.module.pomivy.xml),或直接查找制品文件。

    • 带有元数据文件(.module.pomivy.xml)的模块优先于仅有制品文件的模块。

    • 在一个仓库中找到元数据后,后续仓库将被忽略。

  2. 检索和解析元数据:如果找到元数据,则进行解析。

    • 如果 POM 文件有父 POM,Gradle 会递归解析每个父模块。

  3. 请求制品:模块的所有制品都从提供元数据的同一仓库中获取。

  4. 缓存:所有数据,包括仓库来源和任何可能的遗漏,都存储在依赖缓存中以供将来使用。

上述要点突出了集成Maven Local时可能存在的问题。由于 Maven Local 充当 Maven 缓存,它有时可能会遗漏模块的制品。当 Gradle 从 Maven Local 获取模块且制品缺失时,它会假定这些制品完全不可用。

仓库禁用

当 Gradle 无法从仓库检索信息时,它会禁用该仓库在本次构建的剩余时间内,并使所有依赖解析失败。

此行为确保了可重复性。

如果构建继续进行而忽略了有故障的仓库,一旦仓库恢复在线,后续构建可能会产生不同的结果。

HTTP 重试

在禁用仓库之前,Gradle 会多次尝试连接。如果连接失败,Gradle 会对可能是临时性的特定错误进行重试,重试之间的等待时间会增加。

当仓库无法访问时,会被标记为不可用,可能是由于永久性错误或已用尽最大重试次数。

变体选择

根据构建的要求,Gradle 从元数据中存在的模块变体中选择一个。

具体来说,Gradle 尝试将已解析配置中的属性模块元数据中的属性进行匹配。

变体选择和属性匹配在下一节中详细描述。

可用的 API

ResolutionResult API 提供对已解析依赖图的访问,而无需触发制品下载。

图本身侧重于组件变体,而不是与这些变体关联的制品(文件):

对依赖图的原始访问对于许多用例都很有用:

  • 可视化依赖图,例如为 Graphviz 生成 .dot 文件。

  • 暴露给定解析的诊断信息,类似于 dependenciesdependencyInsight 任务。

  • 当与 ArtifactView API 结合使用时,解析依赖图的制品子集。

考虑以下函数,它从根节点开始遍历依赖图。图中每个节点和边的回调都会被通知。此函数可用作任何需要遍历依赖图的用例的基础:

build.gradle.kts
fun traverseGraph(
    rootComponent: ResolvedComponentResult,
    rootVariant: ResolvedVariantResult,
    nodeCallback: (ResolvedVariantResult) -> Unit,
    edgeCallback: (ResolvedVariantResult, ResolvedVariantResult) -> Unit
) {
    val seen = mutableSetOf<ResolvedVariantResult>(rootVariant)
    nodeCallback(rootVariant)

    val queue = ArrayDeque(listOf(rootVariant to rootComponent))
    while (queue.isNotEmpty()) {
        val (variant, component) = queue.removeFirst()

        // Traverse this variant's dependencies
        component.getDependenciesForVariant(variant).forEach { dependency ->
            val resolved = when (dependency) {
                is ResolvedDependencyResult -> dependency
                is UnresolvedDependencyResult -> throw dependency.failure
                else -> throw AssertionError("Unknown dependency type: $dependency")
            }
            if (!resolved.isConstraint) {
                val toVariant = resolved.resolvedVariant

                if (seen.add(toVariant)) {
                    nodeCallback(toVariant)
                    queue.addLast(toVariant to resolved.selected)
                }

                edgeCallback(variant, toVariant)
            }
        }
    }
}
build.gradle
void traverseGraph(
    ResolvedComponentResult rootComponent,
    ResolvedVariantResult rootVariant,
    Consumer<ResolvedVariantResult> nodeCallback,
    BiConsumer<ResolvedVariantResult, ResolvedVariantResult> edgeCallback
) {
    Set<ResolvedVariantResult> seen = new HashSet<>()
    seen.add(rootVariant)
    nodeCallback(rootVariant)

    def queue = new ArrayDeque<Tuple2<ResolvedVariantResult, ResolvedComponentResult>>()
    queue.add(new Tuple2(rootVariant, rootComponent))
    while (!queue.isEmpty()) {
        def entry = queue.removeFirst()
        def variant = entry.v1
        def component = entry.v2

        // Traverse this variant's dependencies
        component.getDependenciesForVariant(variant).each { dependency ->
            if (dependency instanceof UnresolvedDependencyResult) {
                throw dependency.failure
            }
            if ((!dependency instanceof ResolvedDependencyResult)) {
                throw new RuntimeException("Unknown dependency type: $dependency")
            }

            def resolved = dependency as ResolvedDependencyResult
            if (!dependency.constraint) {
                def toVariant = resolved.resolvedVariant

                if (seen.add(toVariant)) {
                    nodeCallback(toVariant)
                    queue.add(new Tuple2(toVariant, resolved.selected))
                }

                edgeCallback(variant, toVariant)
            }
        }
    }
}

此函数从根变体开始,并对图进行广度优先遍历。ResolutionResult API 是宽松的,因此检查访问的边是未解析(失败)还是已解析非常重要。使用此函数时,对于任何给定节点,节点回调总是在边回调之前调用。

下面,我们利用上述遍历函数将依赖图转换为 .dot 文件以进行可视化:

build.gradle.kts
abstract class GenerateDot : DefaultTask() {

    @get:Input
    abstract val rootComponent: Property<ResolvedComponentResult>

    @get:Input
    abstract val rootVariant: Property<ResolvedVariantResult>

    @TaskAction
    fun traverse() {
        println("digraph {")
        traverseGraph(
            rootComponent.get(),
            rootVariant.get(),
            { node -> println("    ${toNodeId(node)} [shape=box]") },
            { from, to -> println("    ${toNodeId(from)} -> ${toNodeId(to)}") }
        )
        println("}")
    }

    fun toNodeId(variant: ResolvedVariantResult): String {
        return "\"${variant.owner.displayName}:${variant.displayName}\""
    }
}
build.gradle
abstract class GenerateDot extends DefaultTask {

    @Input
    abstract Property<ResolvedComponentResult> getRootComponent()

    @Input
    abstract Property<ResolvedVariantResult> getRootVariant()

    @TaskAction
    void traverse() {
        println("digraph {")
        traverseGraph(
            rootComponent.get(),
            rootVariant.get(),
            node -> { println("    ${toNodeId(node)} [shape=box]") },
            (from, to) -> { println("    ${toNodeId(from)} -> ${toNodeId(to)}") }
        )
        println("}")
    }

    String toNodeId(ResolvedVariantResult variant) {
        return "\"${variant.owner.displayName}:${variant.displayName}\""
    }
}
适当的实现不会使用 println,而是写入输出文件。有关声明任务输入和输出的更多详细信息,请参见编写任务部分。

当我们注册任务时,我们使用 ResolutionResult API 访问 runtimeClasspath 配置的根组件和根变体:

build.gradle.kts
tasks.register<GenerateDot>("generateDot") {
    rootComponent = runtimeClasspath.flatMap {
        it.incoming.resolutionResult.rootComponent
    }
    rootVariant = runtimeClasspath.flatMap {
        it.incoming.resolutionResult.rootVariant
    }
}
build.gradle
tasks.register("generateDot", GenerateDot) {
    rootComponent = configurations.runtimeClasspath.incoming.resolutionResult.rootComponent
    rootVariant = configurations.runtimeClasspath.incoming.resolutionResult.rootVariant
}
此示例使用孵化中的 API。

运行此任务,我们得到以下输出:

digraph {
    "root project ::runtimeClasspath" [shape=box]
    "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" [shape=box]
    "root project ::runtimeClasspath" -> "com.google.guava:guava:33.2.1-jre:jreRuntimeElements"
    "com.google.guava:failureaccess:1.0.2:runtime" [shape=box]
    "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.guava:failureaccess:1.0.2:runtime"
    "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava:runtime" [shape=box]
    "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava:runtime"
    "com.google.code.findbugs:jsr305:3.0.2:runtime" [shape=box]
    "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.code.findbugs:jsr305:3.0.2:runtime"
    "org.checkerframework:checker-qual:3.42.0:runtimeElements" [shape=box]
    "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "org.checkerframework:checker-qual:3.42.0:runtimeElements"
    "com.google.errorprone:error_prone_annotations:2.26.1:runtime" [shape=box]
    "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.errorprone:error_prone_annotations:2.26.1:runtime"
}
dep man adv 3

将其与 dependencies 任务的输出进行比较:

runtimeClasspath
\--- com.google.guava:guava:33.2.1-jre
     +--- com.google.guava:failureaccess:1.0.2
     +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
     +--- com.google.code.findbugs:jsr305:3.0.2
     +--- org.checkerframework:checker-qual:3.42.0
     \--- com.google.errorprone:error_prone_annotations:2.26.1

注意两种表示方式的图是相同的,唯一的区别在于 dot 图中提供了选择的变体信息。