第三章: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 的基础上增加执行顺序约束,但不会改变依赖树的结构。
上图展示了三种元素: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 及其顺序,但不会真的运行。这招帮我解决过很多莫名其妙的执行顺序问题。