Gradle任务与生命周期:从定义到执行的坎坷之路
说实话,很多Gradle新手一上来就被"task"这个词给唬住了。不就是个任务嘛,有什么了不起的?但你真的理解它的全貌吗?我个人习惯把Gradle的构建过程想象成一个"任务调度系统"——你定义一堆活儿,再告诉系统谁先干、谁依赖谁,最后系统按照你定的规矩一个个执行下去。今天我就把这块儿掰开了讲透。
1. Task的定义与声明
任务的定义说白了就三种姿势。你用得最多的肯定是字符串名 + 闭包这种,简单直接。
// 方式一:最常用的闭包风格
task hello {
doLast {
println 'Hello, Gradle!'
}
}
// 方式二:直接指定任务类型
task copyFiles(type: Copy) {
from 'src'
into 'dest'
}
// 方式三:用tasks容器动态创建
tasks.create('myTask') {
doLast { println '动态创建的任务' }
}
我个人习惯用第一种, 因为它可读性最好。但注意一点:闭包内部的代码到底是在配置阶段跑还是执行阶段跑?这事儿我踩过坑。
2. 任务依赖:别让任务"各自为政"
单任务自己玩没意思,真实项目里任务之间往往有前后关系。比如编译之前得先清理,打jar包之前得先编译。
task clean {
doLast { println '清理中...' }
}
task compile {
dependsOn clean
doLast { println '编译中...' }
}
task packageApp {
dependsOn compile
doLast { println '打包中...' }
}
你看这个例子,packageApp 依赖 compile,compile 又依赖 clean。执行 gradle packageApp 时,Gradle 会自动推导出执行顺序:clean → compile → packageApp。你不需要手动指定那个顺序。
gradle task --all 可以查看所有任务以及它们的依赖关系树。排查依赖问题时,这个命令比我手动翻代码快多了。
3. 任务排序:不是依赖,只是"我想让你先跑"
有时候两个任务之间并不是"你离不开我"的关系,只是你希望它先跑。比如两个独立的测试任务,你希望单元测试先跑,集成测试后跑,但集成测试并不依赖单元测试的结果。这时用 mustRunAfter 更合适。
task unitTest {
doLast { println '单元测试' }
}
task integrationTest {
mustRunAfter unitTest
doLast { println '集成测试' }
}
它和 dependsOn 的区别在哪? 这么理解:dependsOn 是强依赖——缺了就不行;mustRunAfter 只是排序——不强制要求执行前置任务,只是如果它俩同时出现,我让你先。我记得有一次为了梳理清楚这两种关系,我把整个项目几十个任务的依赖关系画了张图,才发现很多人把 dependsOn 用成了 mustRunAfter,导致构建链又长又脆。
| 特性 | dependsOn | mustRunAfter |
|---|---|---|
| 是否强制执行前置任务 | 是 | 否 |
| 可用于避免循环依赖 | 否 | 是 |
| 典型场景 | 编译依赖 | 测试优先级排序 |
4. 任务类型:不止是"跑脚本"
Gradle 内置了很多实用的任务类型,比如 Copy、Delete、Zip、Exec。你在声明时直接指定 type,就能免去自己实现一堆逻辑的麻烦。
// 内置类型:Copy
task backupConfig(type: Copy) {
from 'config'
into 'backup/config_backup'
include '*.yml'
}
// 自定义类型:继承 DefaultTask
class MyDeployTask extends DefaultTask {
@TaskAction
void deploy() {
println '执行自定义部署逻辑...'
}
}
task deployTask(type: MyDeployTask)
我自己维护的一个插件项目里, 就设计了好几个自定义任务类型,每个任务只做一件事,然后用 dependsOn 串起来。这样写测试也好测,改逻辑也好改。你想想看,如果所有逻辑都写在一个 task 的 doLast 里,那复用性和可测试性几乎为零。
5. 生命周期的三个阶段
Gradle 的执行过程不是从头到尾平推的,它分成了三个明确的阶段。很多构建问题排查到最后,都是因为对阶段理解有偏差。
Gradle 首先决定哪些项目参与构建,创建 Project 实例。
执行所有项目的 build.gradle 脚本,创建 Task 对象,建立依赖关系图。这里跑的是闭包顶层代码,不是 doFirst/doLast。
根据依赖图,按顺序执行每个 Task 的 doFirst、任务主体、doLast。
// 用代码体验三个阶段
task demo {
println '这是配置阶段执行' // 每次构建都会跑
doFirst {
println '这是执行阶段——第一个执行' // 仅当该任务被执行时跑
}
doLast {
println '这是执行阶段——最后执行' // 同上
}
}
// 测试:gradle demo
// 输出顺序:
// "这是配置阶段执行" ← 配置阶段
// "这是执行阶段——第一个执行" ← doFirst
// "demo 任务无特定动作"
// "这是执行阶段——最后执行" ← doLast
如果你把耗时的网络请求、数据库查询写在 task 闭包的顶层(配置阶段),那你每次执行任何 Gradle 命令都会触发这些操作。我曾经在 CI 上遇到过构建越来越慢,查了半天发现是一个插件在配置阶段做了远程 API 调用。你说冤不冤?
6. 完成阶段(Finished)与钩子
在所有的任务执行完毕之后,Gradle 会触发一个"完成阶段"。你可以通过 gradle.buildFinished 监听这个时机,一般用来做日志统计、生成构建报告、发送通知之类的善后工作。
// 在 build.gradle 或 init.gradle 中注册
gradle.buildFinished { buildResult ->
println "构建完成,结果:${buildResult.failure ? '失败' : '成功'}"
println "构建耗时:${buildResult.executedTasks.size()} 个任务"
}
这个钩子不常用,但一旦遇到需要做全局收尾的场景,它比在每个 task 的 doLast 里重复写日志优雅得多。我参与过一个大型持续集成项目,就是靠 buildFinished 汇总所有子项目的构建状态,然后统一推送到监控面板。
一句话总结
- 定义任务:闭包风格最简单,但注意区分配置阶段代码与执行阶段代码。
- 依赖与排序:dependsOn 绑定执行关系,mustRunAfter 只控制顺序,别搞混。
- 任务类型:多用内置类型,必要时自定义继承 DefaultTask。
- 生命周期:配置阶段创建任务图,执行阶段运行动作,完成阶段收尾。