第三章:Task 定义与依赖——声明任务、任务依赖、排序规则

Gradle 里的 Task 是构建的最小执行单元。说白了,每个 Task 做一件事:编译、打包、测试、清理……一系列 Task 串起来就成了构建流程。我刚开始接触时,觉得写个 Task 不就是声明一个名字加个动作吗?后来踩过坑才明白,真正重要的是怎么让它们按正确顺序跑、怎么避免重复执行。

3.1 声明任务——最基本的姿势

Gradle 提供了两种声明任务的方式。我个人习惯用 DSL 方式,更直观。

// 方式一:通过 tasks 容器注册
tasks.register('hello') {
    doLast {
        println 'Hello, Gradle!'
    }
}

// 方式二:直接用 task 关键字(旧写法,仍可用)
task world {
    doLast {
        println 'World!'
    }
}

需要注意,register 是延迟创建(lazy creation),只有 Task 真正需要执行时才会创建实例。而 task 关键字会立即创建。如果你项目配置比较重,建议用 register 来加速配置阶段。

💡 我的小习惯: 总是用 tasks.register,哪怕只有一个 Task。这样未来扩展时不会有配置阶段的性能隐患。

3.2 任务依赖——dependsOn 的玩法

真实的构建不会只有一个 Task。编译 Task 必须等资源处理完才能跑,单元测试必须等编译完。依赖关系就是通过 dependsOn 建立的。

tasks.register('prepare') {
    doLast {
        println '准备资源...'
    }
}

tasks.register('compile', dependsOn: prepare) {
    doLast {
        println '编译代码...'
    }
}

tasks.register('test', dependsOn: compile) {
    doLast {
        println '运行测试...'
    }
}

当你执行 gradle test 时,Gradle 会自动找出所有上游依赖:test → compile → prepare,并按顺序依次执行。但注意,如果多个 Task 依赖同一个前置 Task,那个前置 Task 只会被执行一次——这是 Gradle 的默认行为。

我曾遇到一个坑:两个无关的 Task 都依赖同一个资源准备 Task,后来发现那个准备 Task 在第一次执行时修改了全局状态,第二次被跳过后导致下游错误。嗯,这里要提醒你:Task 之间尽量少共享可变状态,否则依赖关系会被绕开。

3.3 排序规则——mustRunAfter 与 shouldRunAfter

dependsOn 创建的是“硬依赖”:A 依赖 B,那么 A 必须等到 B 完成后才能跑。mustRunAfter 则是一种“软排序”:它不要求 B 必须先被调用,如果 B 在同一个构建中,则确保 B 在 A 之前运行。

举个例子:你有一个代码格式化 Task 和一个 lint 检查 Task,它们没有直接依赖,但你想让格式化先于 lint 执行。

tasks.register('format') {
    doLast { println '格式化代码...' }
}

tasks.register('lint', dependsOn: 'compile') {
    mustRunAfter('format')
    doLast { println '代码检查...' }
}

如果执行 gradle lint,且 format 没有被直接请求,那么 lint 会直接跑,不会触发 format。但如果执行 gradle format lint,则 format 会在 lint 之前执行。

shouldRunAfter 更弱,它只是“建议”顺序。Gradle 在处理并行执行或复杂的依赖图时,可能不遵循这个提示。我一般只用 mustRunAfter,因为 shouldRunAfter 的语义太模糊,容易让新人困惑。

⚠️ 曾经踩过的坑: 有次我在一个多模块项目里用 mustRunAfter 来控制 A 模块的测试必须在 B 模块的测试之后。结果发现 A 模块和 B 模块的测试没有共同依赖,Gradle 将它们视为两个完全不相关的 Task,mustRunAfter 根本不会生效。后来才意识到——mustRunAfter 只对同时出现在执行列表中的 Task 有效。如果要跨模块强制顺序,还得用 dependsOn。

3.4 任务组与描述——让构建更有条理

当你的项目有几十个自定义 Task 时,gradle tasks 输出的列表会非常乱。建议给你的 Task 设置 group 和 description:

tasks.register('generateDocs') {
    group = 'Documentation'
    description = '生成项目文档'
    doLast {
        println '文档生成完成'
    }
}

这样在 gradle tasks --group=Documentation 时就能只看相关 Task。

属性 说明 示例
group 任务所属的分类名称 'Build', 'Verification', 'Documentation'
description 任务的简短说明 '编译所有模块的源代码'
dependsOn 硬依赖,被依赖 Task 必须先运行 dependsOn: 'compile'
mustRunAfter 软排序,如果同时执行则保证顺序 mustRunAfter('format')
finalizedBy 当前 Task 结束后自动触发后续 Task finalizedBy 'cleanTemp'

3.5 知识结构总览

下面这张图直观展示了 Task 声明、依赖、排序的关系。你看,dependsOn 形成有向无环图(DAG),mustRunAfter 在 DAG 的基础上增加执行顺序约束,但不会改变依赖树的结构。

Task 定义与依赖关系示意 Task 声明 register / task dependsOn 硬依赖 → DAG mustRunAfter 软排序约束 prepare compile test format (软排序) lint 图例 实线:dependsOn 硬依赖 虚线:mustRunAfter 软排序 同层 Task 可并行执行

上图展示了三种元素:register 声明 Task,dependsOn 构建 DAG 保证执行顺序,mustRunAfter 在已有 DAG 基础上增加额外排序约束。注意 format 与 lint 之间没有硬依赖,所以如果只执行 gradle lint,format 不会自动触发。

3.6 实际项目中的依赖组织技巧

大型项目往往有几十个模块,每个模块都有 compile、test、check 等 Task。我习惯在根项目的 build.gradle 中用 subprojects 统一配置基础依赖关系:

subprojects {
    tasks.matching { it.name == 'check' }.configureEach {
        dependsOn 'compileJava'
    }
}

这样所有子模块的 check Task 都会自动依赖自己的 compileJava。如果你不想写死,也可以用 taskName 的命名模式,通过 dependsOn task("compile${capitalize(name)}") 动态计算。

🔑 关键认知:
  • dependsOn 决定了 Task 能否运行——必须前置 Task 先完成。
  • mustRunAfter 只影响“如果两个 Task 都在执行列表里”时的相对顺序。
  • shouldRunAfter 仅在 Gradle 并行执行时被当作提示,不够可靠。
  • Task 的 group 和 description 是最好的文档。

最后提一句:调试依赖关系时可以用 gradle taskName --dry-run,它会列出所有将要执行的 Task 及其顺序,但不会真的运行。这招帮我解决过很多莫名其妙的执行顺序问题。