第2章 项目间任务依赖:dependsOn跨项目引用、依赖子项目任务、避免循环依赖
在多模块 Gradle 项目中,模块间的任务依赖是构建调度的核心。当某个任务需要另一个模块的任务先执行完毕时,必须显式声明跨项目依赖。本章讲解 dependsOn 跨项目引用的正确写法、常见陷阱以及如何检测和预防循环依赖。
2.1 跨项目任务依赖的基本形式
Gradle 允许在一个项目的任务定义中直接依赖另一个项目的任务。语法有两种等价形式:
- 字符串路径 + 任务名:
dependsOn ':submodule:taskName' - 任务引用对象:
dependsOn project(':submodule').tasks.named('taskName')
| 写法 | 解析时机 | 推荐场景 |
|---|---|---|
dependsOn ':submodule:taskName' | 配置阶段解析 | 简单依赖,子项目任务名已知且不会动态变化 |
dependsOn project(':submodule').tasks.named('taskName') | 配置阶段解析(延迟查询) | 需要访问任务属性或注册新依赖时 |
dependsOn project(':submodule').path + ':taskName' | 配置阶段拼接字符串 | 动态构造依赖路径 |
建议:优先使用字符串路径写法,简洁且易读;若需要获取任务输出目录或类型,再使用任务引用对象。
2.2 实际配置示例
假设项目结构如下:
project-root/
├── settings.gradle
├── build.gradle (root)
├── module-a/
│ └── build.gradle
└── module-b/
└── build.gradle
在 settings.gradle 中:
rootProject.name = 'multi-module-demo'
include 'module-a', 'module-b'
在 module-a/build.gradle 中定义一个自定义任务:
task compileModuleA {
doLast {
println 'Module A compiled'
}
}
在根项目 build.gradle 中需要依赖 module-a 的 compileModuleA 任务:
task buildAll {
dependsOn ':module-a:compileModuleA'
doLast {
println 'All modules built'
}
}
执行 ./gradlew buildAll 后,Gradle 会先执行 module-a:compileModuleA,再执行根项目的 buildAll。
2.3 依赖子项目任务的进阶技巧
- 依赖多个任务:
dependsOn ':module-a:task1', ':module-b:task2' - 依赖子项目所有同名任务:可使用
tasks.matching { it.name == 'check' }.forEach { task -> dependsOn task } - 仅在特定条件下依赖:通过
onlyIf或dependsOnTaskDidWork控制
2.4 避免循环依赖
循环依赖指 A 任务依赖 B,B 又直接或间接依赖 A。Gradle 会在配置阶段检测并抛出异常:
Circular dependency between tasks: :module-a:taskA :module-b:taskB ...
2.4.1 常见循环依赖场景
- 任务 A 依赖 B,任务 B 依赖 A(直接循环)
- 任务 A 依赖 B,B 依赖 C,C 又依赖 A(间接循环)
- 通过
finalizedBy不当使用也可能产生循环
2.4.2 检测与调试方法
| 工具/方法 | 命令 | 作用 |
|---|---|---|
| —dry-run | ./gradlew taskName --dry-run | 模拟执行,展示计划的任务顺序,不实际运行 |
| —info 日志 | ./gradlew taskName --info 2>&1 | grep -i 'circular' | 输出任务图构建细节 |
| 依赖图谱可视化 | ./gradlew :module-a:taskA --graph(需插件) | 输出 DOT 格式依赖图 |
| TaskExecutionGraph API | 在 gradle.taskGraph.whenReady 中遍历 | 编程方式检测循环引用 |
危险操作:不要使用
dependsOn tasks 这样不加区分的写法,可能意外引入循环依赖。始终明确依赖的具体任务和项目。
2.4.3 解决循环依赖的策略
- 提取公共任务:将 A 和 B 共同依赖的部分抽取到单独模块或 Lifecycle 任务中。
- 使用
mustRunAfter代替dependsOn:如果只需要执行顺序而不需要硬依赖,用mustRunAfter可以打破循环。 - 延迟依赖注册:在
afterEvaluate或gradle.taskGraph.whenReady中注册,让配置阶段先完成。 - 重新设计模块边界:循环依赖往往是模块职责划分不清晰的信号,应考虑重构。
2.5 任务依赖关系图
下图展示一个典型的多模块任务依赖关系,并标注了避免循环依赖的注意点:
2.6 调试技巧与常见错误
- 使用 —dry-run 验证依赖顺序:执行
./gradlew buildAll --dry-run,输出会按执行顺序列出所有任务,可以快速确认依赖是否正确。 - 使用 —info 查看任务图构建细节:加
--info参数会输出类似Adding task ':module-a:compile' as a dependency of ':buildAll'的信息。 - 常见错误:依赖不存在任务:如果被依赖的任务未被定义,Gradle 会报
Task with path '...' not found。解决方案:检查子项目是否包含该任务,或使用tasks.register提前注册。 - 常见错误:配置顺序敏感:如果使用
project(':submodule').tasks.named('taskName'),但子项目尚未配置,可以放在afterEvaluate中:
afterEvaluate {
task dependOnSub {
dependsOn project(':module-a').tasks.named('compile')
}
}
2.7 性能优化要点
| 场景 | 建议 | 原因 |
|---|---|---|
| 大量依赖同个子项目的同一个任务 | 使用 dependsOn tasks.withType(SomeType) 而非逐一列举 | 减少配置阶段任务引用创建次数 |
| 跨项目依赖配置过多 | 启用 配置缓存(gradle.property cache=true) | 避免每次运行重新解析跨项目任务图 |
| 循环依赖导致构建失败 | 重构模块边界,分离共享逻辑 | 彻底消除循环,提升可维护性 |
2.8 小结
跨项目任务依赖是 Gradle 多模块调度的核心能力。正确使用 dependsOn 可以在一次构建中串联多个模块的任务链。务必注意:
- 优先使用字符串路径写法
':module:taskName' - 避免循环依赖,使用
mustRunAfter作为替代方案 - 利用
--dry-run和--info尽早验证依赖图 - 在
afterEvaluate或任务图事件中注册延迟依赖以防配置顺序问题