在依赖图解析阶段,Gradle 会构建一个已解析的依赖图,它建模了不同组件及其变体之间的关系。
模块
依赖图解析始于构建脚本中声明的依赖
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
是版本。
元数据
组件由元数据详细描述,元数据可在托管该组件的仓库中获取,格式为 ivy
、pom
或 GMM
元数据。
{
"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 所需的依赖。
变体 | 依赖 | 制品 |
---|---|---|
|
|
|
|
|
|
… 其他变体 … |
… 一些依赖 … |
… 一些制品 … |
每个变体都包含一组制品并定义了一组依赖(即被视为构建的传递性依赖)
-
com.fasterxml.jackson.core:jackson-databind:2.17.2
的runtimeElements
变体-
依赖于
com.fasterxml.jackson.core
。 -
提供一个名为
jackson-databind-2.17.2.jar
的制品。
-
为了区分 apiElements
和 runtimeElements
变体,Gradle 使用属性。
属性
为了区分变体,Gradle 使用属性。
属性用于定义变体的特定特征或属性,以及应使用这些变体的上下文。
在 Jackson Databind 的元数据中,我们看到 runtimeElements
变体由 org.gradle.category
、org.gradle.dependency.bundling
、org.gradle.libraryelement
和 org.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 是如何构建依赖图的呢?
依赖图解析流程
依赖图解析以节点逐个(即,变体逐个)的方式操作。
循环的每次迭代一次处理一个节点,从将一个节点出队列开始。
最初,队列是空的。当进程启动时,一个根节点被添加到队列中。根节点实际上就是可解析配置

Gradle 通过从队列中拉出根节点来开始循环。Gradle 检查根节点的依赖,解决它们的冲突,并下载它们的元数据。根据它们的元数据,Gradle 选择这些依赖的变体并将其添加回队列中。
根变体的依赖对应于可解析配置声明的依赖。 |
此时,队列包含根节点的依赖的所有选定变体,这些变体现在将逐个处理。
对于循环中的每个节点,Gradle 会:
-
评估其依赖。
-
使用冲突解决确定其目标版本。
-
将变体添加到顶级队列。
循环重复进行,直到节点队列为空。一旦过程完成,依赖图就已解析。
依赖图解析在并行元数据下载和单线程逻辑之间交替进行,对于单个图重复此模式。 |
冲突解决
在执行依赖解析时,Gradle 处理两种类型的冲突:
-
版本冲突:当多个依赖请求相同的依赖但版本不同时发生。Gradle 必须选择将哪个版本包含在图中。
-
实现/功能冲突:当依赖图包含提供相同功能或功能的不同模块时发生。Gradle 通过选择一个模块来避免重复实现来解决这些冲突。
依赖解析过程高度可定制,许多 API 可以影响此过程。
A. 版本冲突
当两个组件出现以下情况时,可能会发生版本冲突:
-
依赖于同一个模块,例如
com.google.guava:guava
-
但版本不同,例如
20.0
和25.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.0
,beta
是限定符。不带限定符的版本被认为更稳定,因此 Gradle 会优先选择它们。
这里有一些示例进行说明:
-
1.0.0
(无限定符) -
1.0.0-beta
(限定符:beta
) -
2.1-rc1
(限定符:rc1
)
即使限定符的字典序更高,Gradle 通常也会认为 1.0.0
这样的版本高于 1.0.0-beta
。
在解决版本冲突时,Gradle 应用以下逻辑:
-
基本版本比较: Gradle 首先选择具有最高基本版本(忽略任何限定符)的版本。所有其他版本都被丢弃。
-
限定符处理: 如果仍然存在具有相同基本版本的多个版本,Gradle 会优先选择不带限定符的版本(即发布版本)。如果所有版本都带有限定符,Gradle 会考虑限定符的顺序,优先选择更稳定的(如 "release"),而不是 "beta" 或 "alpha" 等。
B. 实现/功能冲突
在以下场景中会发生冲突:
-
不兼容变体:当两个模块尝试选择依赖的不同且不兼容的变体时。
-
相同功能:当多个模块声明相同的功能,造成功能上的重叠时。
此类冲突在下面描述的变体选择期间解决。
元数据检索
Gradle 在依赖图中需要模块元数据有两个原因:
-
确定动态依赖的现有版本:当指定了动态版本(如
1.+
或latest.release
)时,Gradle 必须识别可用的具体版本。 -
解析特定版本的模块依赖:Gradle 根据指定版本检索与模块关联的依赖,确保将正确的传递性依赖包含在构建中。
A. 确定动态依赖的现有版本
当遇到动态版本时,Gradle 必须通过以下步骤识别可用的具体版本:
-
检查仓库:Gradle 按照添加顺序检查每个定义的仓库。它不会在第一个返回元数据的仓库处停止,而是继续检查所有可用仓库。
-
Maven 仓库:Gradle 从
maven-metadata.xml
文件检索版本信息,该文件列出了可用版本。 -
Ivy 仓库:Gradle 借助目录列表收集可用版本。
B. 解析特定版本的模块依赖
当 Gradle 尝试解析特定版本的所需依赖时,它遵循此过程:
-
仓库检查:Gradle 按照定义的顺序检查每个仓库。
-
它查找描述模块的元数据文件(
.module
、.pom
或ivy.xml
),或直接查找制品文件。 -
带有元数据文件(
.module
、.pom
或ivy.xml
)的模块优先于仅有制品文件的模块。 -
在一个仓库中找到元数据后,后续仓库将被忽略。
-
-
检索和解析元数据:如果找到元数据,则进行解析。
-
如果 POM 文件有父 POM,Gradle 会递归解析每个父模块。
-
-
请求制品:模块的所有制品都从提供元数据的同一仓库中获取。
-
缓存:所有数据,包括仓库来源和任何可能的遗漏,都存储在依赖缓存中以供将来使用。
上述要点突出了集成Maven Local时可能存在的问题。由于 Maven Local 充当 Maven 缓存,它有时可能会遗漏模块的制品。当 Gradle 从 Maven Local 获取模块且制品缺失时,它会假定这些制品完全不可用。 |
可用的 API
ResolutionResult
API 提供对已解析依赖图的访问,而无需触发制品下载。
图本身侧重于组件变体,而不是与这些变体关联的制品(文件):
-
ResolvedComponentResult
- 表示原始依赖图中的已解析组件。 -
ResolvedVariantResult
- 表示原始依赖图中的已解析变体。
对依赖图的原始访问对于许多用例都很有用:
-
可视化依赖图,例如为 Graphviz 生成
.dot
文件。 -
暴露给定解析的诊断信息,类似于
dependencies
或dependencyInsight
任务。 -
当与
ArtifactView
API 结合使用时,解析依赖图的制品子集。
考虑以下函数,它从根节点开始遍历依赖图。图中每个节点和边的回调都会被通知。此函数可用作任何需要遍历依赖图的用例的基础:
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)
}
}
}
}
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
文件以进行可视化:
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}\""
}
}
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
配置的根组件和根变体:
tasks.register<GenerateDot>("generateDot") {
rootComponent = runtimeClasspath.flatMap {
it.incoming.resolutionResult.rootComponent
}
rootVariant = runtimeClasspath.flatMap {
it.incoming.resolutionResult.rootVariant
}
}
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" }

将其与 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 图中提供了选择的变体信息。
下一步: 了解变体选择和属性匹配 >>