5、Task详解:Task的创建与配置、Task依赖与排序、Task类型、自定义Task类
Task 是 Gradle 构建的核心执行单元。说白了,构建脚本里写的那些「干活」的代码,最终都要封装成 Task 来运行。我刚开始用 Gradle 时,总觉得 Task 就是个简单的任务定义,后来踩了不少坑才明白——Task 的创建、配置、依赖关系,以及如何自定义 Task 类型,这些才是构建自动化的基本功。
这一章,咱们就把 Task 彻底讲透。你想想看,如果连 Task 都玩不转,那构建脚本写得再花哨,也只是空中楼阁。
5.1 Task 的创建与配置
创建 Task 的方式有好几种。我个人最常用的是 tasks.register() 方法,尤其是在 Gradle 4.9 之后,这种方式成了推荐做法。
// 方式一:使用 register(推荐)
tasks.register('hello') {
doLast {
println 'Hello, Gradle!'
}
}
// 方式二:使用 create(传统方式)
tasks.create('helloOld') {
doLast {
println 'Hello from old way!'
}
}
这里有个关键区别:register 是延迟创建的,Task 只有在真正需要时才会被实例化。而 create 会立即创建。我在项目中遇到过因为用了 create 导致配置阶段变慢的情况,后来全部改成了 register,构建速度提升了不少。
配置 Task 有两种时机:
- 配置时配置:在 Task 创建时直接写配置逻辑
- 执行时配置:使用
doFirst/doLast添加动作
tasks.register('configDemo') {
// 配置时执行
description = '这是一个配置示例'
// 执行时执行
doFirst {
println '任务开始前执行'
}
doLast {
println '任务结束后执行'
}
}
5.2 Task 依赖与排序
Task 之间的执行顺序,说白了就两种关系:依赖和排序。
依赖(dependsOn):A 依赖 B,那么 B 必须在 A 之前执行。如果 B 失败了,A 也不会执行。
排序(mustRunAfter / shouldRunAfter):只是指定顺序,不强制依赖关系。A 排在 B 后面,但如果 A 和 B 没有依赖关系,它们可能根本不会同时出现。
tasks.register('taskA') {
doLast { println '执行 A' }
}
tasks.register('taskB') {
dependsOn taskA // B 依赖 A
doLast { println '执行 B' }
}
tasks.register('taskC') {
mustRunAfter taskA // C 排在 A 后面
doLast { println '执行 C' }
}
我曾经犯过一个错误:用 dependsOn 来强行排序两个没有逻辑依赖的任务。结果导致构建图变得很复杂,并行执行时反而更慢了。后来改用 mustRunAfter,问题就解决了。
mustRunAfter 和 shouldRunAfter 的区别在于——前者是强约束,如果出现循环排序会报错;后者是弱约束,Gradle 在无法满足时会忽略它。
5.3 Task 类型
Gradle 内置了很多实用的 Task 类型。你不需要每次都从零开始写。我整理了几个最常用的:
| Task 类型 | 用途 | 典型场景 |
|---|---|---|
Copy |
复制文件 | 将资源文件复制到输出目录 |
Delete |
删除文件或目录 | 清理构建产物 |
Exec |
执行外部命令 | 调用系统工具或脚本 |
Zip / Tar |
打包压缩 | 生成发布包 |
JavaExec |
运行 Java 程序 | 启动应用或测试 |
// 使用 Copy 类型
tasks.register('copyResources', Copy) {
from 'src/main/resources'
into 'build/resources'
include '**/*.properties'
}
// 使用 Exec 类型
tasks.register('runScript', Exec) {
commandLine 'bash', '-c', 'echo "Hello from shell"'
}
// 使用 JavaExec 类型
tasks.register('runApp', JavaExec) {
mainClass = 'com.example.Main'
classpath = sourceSets.main.runtimeClasspath
}
核心要点:使用内置 Task 类型时,你只需要配置它的「属性」,不需要写 doLast。因为这些类型的执行逻辑已经封装好了。比如 Copy 的 from 和 into 就是它的配置属性。
5.4 自定义 Task 类
当内置类型不够用时,就该自己写 Task 类了。嗯,这里要注意:自定义 Task 类不是写一个闭包就完事了,而是要创建一个真正的类。
// 在 buildSrc 或项目中定义
abstract class MyTask extends DefaultTask {
@Input
abstract Property<String> getMessage()
@OutputFile
abstract RegularFileProperty getOutputFile()
@TaskAction
void doWork() {
def msg = message.get()
def file = outputFile.get().asFile
file.text = "Message: $msg"
logger.quiet "已写入文件: $file"
}
}
// 使用自定义 Task
tasks.register('myTask', MyTask) {
message = 'Hello from custom task!'
outputFile = layout.buildDirectory.file('output.txt')
}
这里有几个关键点:
- 继承
DefaultTask:这是所有自定义 Task 的基类 - 使用
@TaskAction:标记执行方法,Gradle 会自动调用 - 使用
@Input/@OutputFile:声明输入输出,这是增量构建的基础 - 使用
Property类型:而不是普通的String或File,这样才能支持延迟配置
String message 而不是 Property<String>。结果在配置时一切正常,但到了执行时,message 的值总是 null。原因就是没有使用延迟属性。记住:在 Gradle 的 Task 类里,所有可配置的属性都应该用 Property 类型。
另外,自定义 Task 类最好放在 buildSrc 目录下,这样整个项目都能复用。如果只是单个脚本用,也可以直接写在 build.gradle 里,但我不建议这么做——代码会变得很臃肿。
@TaskAction 方法开始。等跑通了,再逐步添加 @Input、@Output 等注解。别一上来就想搞个大而全的 Task,容易把自己绕进去。
好了,Task 的核心内容就这些。从创建配置,到依赖排序,再到内置类型和自定义类——每一步都有它的设计哲学。你想想看,Gradle 之所以强大,很大程度上就是因为它把「任务」这个抽象概念做得足够灵活。掌握了 Task,你就掌握了 Gradle 构建的「原子操作」。