4. Task详解:Task的创建与配置、Task依赖与排序、增量构建与缓存、Task类型与自定义Task
Task 是 Gradle 构建脚本里的基本执行单元。说白了,你写的每一个构建动作——编译、打包、测试、部署——最终都会映射成一个或多个 Task。我刚开始用 Gradle 时,总觉得 Task 不就是个任务嘛,跑就完了。后来在大型项目里踩了不少坑,才真正理解 Task 的设计哲学。
这一章,我会把 Task 的方方面面拆开来讲。从创建配置,到依赖排序,再到增量构建和自定义类型,咱们一步步来。
4.1 Task 的创建与配置
创建 Task 的方式有好几种。我个人最常用的是通过 tasks.register() 方法,这种方式是延迟创建的,性能更好。
// 方式一:使用 register(推荐)
tasks.register('hello') {
doLast {
println 'Hello, Gradle!'
}
}
// 方式二:使用 create(立即创建)
tasks.create('helloImmediate') {
doLast {
println 'Hello immediately!'
}
}
// 方式三:通过 TaskContainer
tasks.register('greet') {
description = '一个打招呼的任务'
group = 'MyTasks'
doLast {
println "你好,${project.name}!"
}
}
配置 Task 也很灵活。你可以在注册时直接配置,也可以事后通过 named() 来配置。
tasks.register('configurableTask') {
// 注册时配置
ext.customProp = '初始值'
}
// 事后配置
tasks.named('configurableTask') {
ext.customProp = '修改后的值'
doLast {
println "属性值: ${customProp}"
}
}
我的习惯: 能用 register 就别用 create。延迟创建在配置阶段开销小,尤其是项目里有上百个 Task 时,差距很明显。
4.2 Task 依赖与排序
Task 之间往往有先后关系。比如,测试前得先编译,打包前得先测试。Gradle 提供了两种方式来控制执行顺序:依赖 和 排序。
4.2.1 依赖(dependsOn)
依赖意味着一个 Task 必须在另一个 Task 之后执行。如果被依赖的 Task 失败了,当前 Task 也不会执行。
tasks.register('taskA') {
doLast { println '执行 Task A' }
}
tasks.register('taskB') {
dependsOn 'taskA' // taskB 依赖 taskA
doLast { println '执行 Task B' }
}
// 执行 taskB 时,会自动先执行 taskA
4.2.2 排序(mustRunAfter / shouldRunAfter)
排序和依赖不同。排序只是规定顺序,不强制要求执行。如果两个 Task 都被包含在执行列表中,排序规则才会生效。
tasks.register('taskX') {
doLast { println 'Task X' }
}
tasks.register('taskY') {
mustRunAfter 'taskX' // 如果同时执行,taskY 在 taskX 之后
doLast { println 'Task Y' }
}
// 执行 gradle taskY taskX,顺序是 taskX -> taskY
我曾经踩过的坑: 把 mustRunAfter 当 dependsOn 用。结果 taskX 没执行,taskY 却跑了,因为排序不触发执行。记住:排序只是排队,依赖才是叫号。
4.2.3 依赖与排序的对比
| 特性 | dependsOn | mustRunAfter | shouldRunAfter |
|---|---|---|---|
| 是否触发执行 | 是 | 否 | 否 |
| 失败时是否中断 | 是 | 否 | 否 |
| 强制执行 | 是 | 是 | 否(可被覆盖) |
| 典型场景 | 编译后测试 | 清理后构建 | 日志输出顺序 |
4.3 增量构建与缓存
这是 Gradle 最让我佩服的地方。你想想看,如果每次构建都重新编译所有代码,那得多慢?增量构建就是为了解决这个问题。
4.3.1 增量构建的原理
Gradle 通过 输入(Inputs) 和 输出(Outputs) 来判断 Task 是否需要重新执行。如果输入没变,输出也没变,那这个 Task 就可以跳过。
tasks.register('processData') {
// 声明输入
inputs.file('data/input.txt')
// 声明输出
outputs.file('build/output.txt')
doLast {
def input = file('data/input.txt').text
file('build/output.txt').text = input.toUpperCase()
println '数据处理完成'
}
}
第一次执行时,Gradle 会记录输入文件的哈希值。第二次执行时,如果 input.txt 没变,Task 会被标记为 UP-TO-DATE,直接跳过。
4.3.2 缓存(Build Cache)
缓存比增量构建更进一步。增量构建只在本地有效,缓存可以跨机器共享。比如 CI 服务器构建了一次,开发人员可以直接拉取缓存结果。
// settings.gradle
buildCache {
local {
enabled = true
directory = new File(rootDir, 'build-cache')
}
remote(HttpBuildCache) {
url = 'https://cache.example.com/'
push = true
}
}
核心要点: 想让 Task 支持缓存,必须正确声明 inputs 和 outputs。少声明一个,缓存就可能失效。我在项目中见过有人漏了 inputs.files,结果每次构建都全量执行,白白浪费了时间。
4.3.3 如何让自定义 Task 支持增量
abstract class IncrementalTask extends DefaultTask {
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@TaskAction
void process() {
def input = inputFile.get().asFile.text
def output = new File(outputDir.get().asFile, 'result.txt')
output.text = input.reverse()
logger.lifecycle('增量处理完成')
}
}
tasks.register('reverseText', IncrementalTask) {
inputFile = file('source.txt')
outputDir = file('build/output')
}
嗯,这里要注意:@InputFile 和 @OutputDirectory 这些注解是 Gradle 识别输入输出的关键。少了它们,增量构建就不起作用。
4.4 Task 类型与自定义 Task
Gradle 内置了很多 Task 类型,比如 Copy、Delete、Exec、Zip 等。但真实项目里,我们经常需要自定义 Task 类型。
4.4.1 内置 Task 类型的使用
tasks.register('backupConfig', Copy) {
from 'src/main/resources/config'
into 'build/backup'
include '*.properties'
}
tasks.register('cleanBuild', Delete) {
delete 'build', 'dist'
}
tasks.register('runScript', Exec) {
commandLine 'python', 'scripts/deploy.py'
}
4.4.2 自定义 Task 类型
当内置类型不够用时,就该自己动手了。我建议用抽象类 + @TaskAction 注解的方式,这样最规范。
abstract class ReportTask extends DefaultTask {
@Input
abstract Property<String> getReportName()
@InputFiles
abstract ConfigurableFileCollection getSourceFiles()
@OutputFile
abstract RegularFileProperty getReportFile()
@TaskAction
void generateReport() {
def report = new StringBuilder()
report.appendLine("报告名称: ${reportName.get()}")
report.appendLine("生成时间: ${new Date()}")
sourceFiles.each { file ->
report.appendLine("文件: ${file.name} - ${file.length()} bytes")
}
reportFile.get().asFile.text = report.toString()
logger.lifecycle("报告已生成: ${reportFile.get()}")
}
}
// 使用自定义 Task
tasks.register('generateSummary', ReportTask) {
reportName = '项目摘要'
sourceFiles.from(fileTree('src').include('**/*.java'))
reportFile = file('build/reports/summary.txt')
}
我的建议: 自定义 Task 时,尽量用 Property 和 RegularFileProperty 这类延迟属性。它们比直接赋值更灵活,也更容易支持增量构建。
4.4.3 自定义 Task 的最佳实践
- 使用抽象类:Gradle 会为抽象属性生成实现,省去手动赋值的麻烦。
- 声明所有输入输出:漏掉一个,增量构建就可能失效。
- 用 @TaskAction 标记执行方法:别在构造函数里写逻辑。
- 提供合理的默认值:让使用者少配置一些。
- 添加 group 和 description:方便在
gradle tasks中查看。
4.5 知识体系总览
下面这张图,我把 Task 的核心知识点串了起来。你可以把它当作一个快速索引。
Task 是 Gradle 构建的基石。掌握了创建、依赖、增量和自定义这四块,你就能写出高效、可维护的构建脚本。我在项目中见过太多人把 Task 写成大泥球,几百行代码塞在一个 doLast 里。其实只要拆分成多个小 Task,加上合理的依赖和增量配置,构建速度能提升好几倍。
记住:好的构建脚本,不是功能多,而是该快的时候快,该省的时候省。
公众号:蓝海资料掘金营,微信deep3321