8、自定义任务:DefaultTask与自定义任务类、@TaskAction、任务分组与描述、跳过条件、任务超时
说实话,Gradle 脚本写多了,你会发现内置任务虽然够用,但总有那么些场景——比如自动生成代码、批量处理资源、或者对接内部工具链——非得自己动手写任务不可。我记得刚接手一个遗留项目时,构建脚本里塞了三百多行 Groovy,全是各种 doLast 和 doFirst 嵌套,看得我头皮发麻。后来我下定决心,把所有逻辑都抽成了自定义任务类,脚本瞬间清爽了,维护起来也舒服多了。
这一节,我们就来聊聊怎么用 Kotlin 写一个正经的自定义任务。不是那种在 build.gradle.kts 里随便写几行的临时任务,而是有类、有注解、有分组、有超时控制的「正规军」。
8.1 从 DefaultTask 开始
Gradle 里所有任务都继承自 DefaultTask。你平时在脚本里用 tasks.register 创建的任务,底层其实也是它的子类实例。但如果你想封装逻辑、复用代码,就得自己写一个类。
abstract class MyCustomTask : DefaultTask() {
@TaskAction
fun doWork() {
println("Hello from MyCustomTask!")
}
}
然后在 build.gradle.kts 里注册:
tasks.register<MyCustomTask>("helloTask")
跑一下 ./gradlew helloTask,控制台就会打印那句话。嗯,就这么简单。
但这里有个细节:@TaskAction 注解标记的方法,才是任务真正执行时调用的入口。一个任务类里可以有多个方法,但 @TaskAction 只能有一个。我在项目中见过有人把业务逻辑写在构造函数里,结果每次配置阶段都跑一遍,构建慢得离谱。记住,@TaskAction 只在执行阶段触发。
8.2 任务分组与描述
光有任务名还不够。你想想看,当项目大了,任务列表一长串,没有分组和描述,找起来跟大海捞针似的。Gradle 提供了两个属性来帮我们整理:
group:任务所属分组,比如 "build"、"verification"、"documentation"description:任务的简短说明
我习惯在注册任务时直接指定:
tasks.register<MyCustomTask>("helloTask") {
group = "my-tools"
description = "打印一条问候信息"
}
这样你执行 ./gradlew tasks 时,就能看到它乖乖待在 "my-tools" 分组下面,后面还跟着描述文字。说白了,这就是给任务贴了个标签,方便自己和队友快速定位。
如果你想让分组更清晰,还可以在任务类里用 @TaskGroup 和 @TaskDescription 注解来声明默认值:
abstract class MyCustomTask : DefaultTask() {
@get:TaskGroup
val groupName: String = "my-tools"
@get:TaskDescription
val taskDesc: String = "打印一条问候信息"
@TaskAction
fun doWork() {
println("Hello from MyCustomTask!")
}
}
这样注册时就不用每次都写了。不过我个人更倾向于在注册处显式指定,因为不同模块可能对同一个任务有不同的分组需求。
8.3 跳过条件:让任务「偷懒」
有时候任务不需要每次都执行。比如,只有某个文件发生变化时才运行,或者某个环境变量没设置时就跳过。Gradle 提供了两种跳过方式:
8.3.1 使用 onlyIf
onlyIf 是一个闭包,返回 true 才执行任务。我在项目中经常用它来判断输入文件是否存在:
tasks.register<MyCustomTask>("conditionalTask") {
onlyIf {
val inputFile = file("input.txt")
if (!inputFile.exists()) {
logger.warn("input.txt 不存在,跳过任务")
false
} else {
true
}
// 或者更简洁:file("input.txt").exists()
}
}
注意,onlyIf 是在任务执行前判断的。如果返回 false,任务会被标记为 SKIPPED,不会执行 @TaskAction 里的代码。
8.3.2 使用 StopExecutionException
另一种方式是在任务执行过程中主动抛出 StopExecutionException。这适合那些需要在运行时动态判断的场景:
abstract class MyCustomTask : DefaultTask() {
@TaskAction
fun doWork() {
if (!project.file("input.txt").exists()) {
throw StopExecutionException("input.txt 不存在,跳过执行")
}
println("开始处理...")
}
}
这两种方式有什么区别?onlyIf 更优雅,适合声明式的条件;而 StopExecutionException 更灵活,可以在方法中间任意位置跳出。我曾经在一个代码生成任务里用过后者,因为生成逻辑分好几步,某一步发现前置条件不满足时直接抛异常,比在开头写一大串 if 判断要清晰得多。
8.4 任务超时:防止「死循环」
你有没有遇到过这种情况:某个任务跑着跑着就卡住了,可能是网络请求超时,也可能是逻辑里有死循环。没有超时控制,构建进程就会一直挂在那,浪费 CI 时间。
Gradle 提供了 timeout 属性,单位是 Duration:
tasks.register<MyCustomTask>("longRunningTask") {
timeout = Duration.ofSeconds(30)
}
如果任务执行超过 30 秒,Gradle 会强制终止它,并标记为失败。我在处理大型资源打包任务时,一定会设置超时。因为有一次,某个资源文件意外变成了 2GB,打包脚本直接跑了一个多小时还没结束,CI 管道被堵得死死的。从那以后,凡是可能耗时的任务,我都加上了超时限制。
你可以在任务类里设置默认超时:
abstract class MyCustomTask : DefaultTask() {
init {
timeout = Duration.ofMinutes(5)
}
@TaskAction
fun doWork() {
// 耗时操作
}
}
这样所有注册的实例默认都有 5 分钟超时,但注册时仍然可以覆盖:
tasks.register<MyCustomTask>("quickTask") {
timeout = Duration.ofSeconds(10)
}
8.5 综合示例:一个带跳过和超时的代码生成任务
把上面这些知识点串起来,写一个实际点的任务。假设我们要根据一个配置文件生成 Kotlin 数据类:
abstract class CodeGenTask : DefaultTask() {
@get:InputFile
abstract val configFile: RegularFileProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
init {
// 默认超时 2 分钟
timeout = Duration.ofMinutes(2)
}
@TaskAction
fun generate() {
val config = configFile.get().asFile
if (!config.exists()) {
throw StopExecutionException("配置文件 $config 不存在,跳过生成")
}
val out = outputDir.get().asFile
out.mkdirs()
// 模拟生成逻辑
val content = "data class GeneratedClass(val name: String)"
val targetFile = File(out, "GeneratedClass.kt")
targetFile.writeText(content)
logger.lifecycle("已生成: ${targetFile.absolutePath}")
}
}
注册时:
tasks.register<CodeGenTask>("generateCode") {
configFile.set(layout.projectDirectory.file("config.json"))
outputDir.set(layout.buildDirectory.dir("generated"))
onlyIf {
// 只有 release 构建才执行
project.gradle.startParameter.taskNames.any { it.contains("release") }
}
}
这个任务有输入输出声明、有超时、有跳过条件,算是一个比较完整的自定义任务了。
8.6 知识体系一览
为了让你更直观地理解这一节的内容,我画了一张图:
@SkipWhen 注解。不过我个人觉得 onlyIf 更直观,团队里其他人一看就懂。
好了,这一节的内容就这些。自定义任务其实不难,关键是把「类结构」、「执行逻辑」、「控制条件」这三块理清楚。下次你在脚本里写 doLast 之前,不妨想想:是不是该抽成一个正经的任务类了?