图解析阶段,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”、“module”和“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

请注意,两种表示形式的图是相同的,唯一的区别在于点图中可用的选定变体信息。