第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 解决循环依赖的策略

  1. 提取公共任务:将 A 和 B 共同依赖的部分抽取到单独模块或 Lifecycle 任务中。
  2. 使用 mustRunAfter 代替 dependsOn:如果只需要执行顺序而不需要硬依赖,用 mustRunAfter 可以打破循环。
  3. 延迟依赖注册:在 afterEvaluate 或 gradle.taskGraph.whenReady 中注册,让配置阶段先完成。
  4. 重新设计模块边界:循环依赖往往是模块职责划分不清晰的信号,应考虑重构。

2.5 任务依赖关系图

下图展示一个典型的多模块任务依赖关系,并标注了避免循环依赖的注意点:

多模块任务依赖关系与循环依赖避免 root:buildAll (入口任务) module-a:compile (编译) module-b:test (测试) module-a:jar (打包) module-b:check (检查) ✘ 禁止:module-a:jar → module-b:check → module-a:jar 循环依赖避免要点 · 依赖方向应保持单向(从上到下或从左到右) · 若已有双向关系,使用 mustRunAfter 代替 dependsOn 打破硬依赖 · 使用 dry-run 或 info 日志预先检查依赖图

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 或任务图事件中注册延迟依赖以防配置顺序问题