6、自定义Task:创建自定义Task类、使用@TaskAction注解、Task配置避免
说实话,Gradle 的 Task 系统是构建脚本的灵魂。但很多同学写脚本时,习惯把一堆逻辑塞进 doLast 或 doFirst 里。脚本一长,维护起来就头疼了。
我个人习惯是:只要逻辑稍微复杂一点,就抽成自定义 Task 类。这样代码清晰,还能复用。今天我们就聊聊怎么优雅地创建自定义 Task。
6.1 为什么要自定义 Task 类?
你想想看,如果每个构建逻辑都写在 build.gradle.kts 里,那文件会膨胀到什么程度?我见过一个项目,构建脚本 2000 多行,找一段逻辑得翻半天。
自定义 Task 类的好处很明显:
- 类型安全:Kotlin 编译器帮你检查类型,写错了 IDE 直接报红
- 可测试:Task 类可以单独写单元测试
- 可复用:同一个 Task 类可以在多个项目中使用
- 可配置:通过属性暴露配置项,灵活度更高
核心原则:Task 类只负责「做什么」,不负责「什么时候做」。配置和执行要分开。
6.2 创建你的第一个自定义 Task 类
在 Gradle 中,自定义 Task 类本质上就是继承 DefaultTask 的 Kotlin 类。来看个最简单的例子:
// buildSrc/src/main/kotlin/com/example/HelloTask.kt
package com.example
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
open class HelloTask : DefaultTask() {
@TaskAction
fun greet() {
println("Hello from ${project.name}!")
}
}
然后在 build.gradle.kts 里注册使用:
tasks.register<com.example.HelloTask>("sayHello") {
// 这里可以配置 Task 的属性
}
运行 ./gradlew sayHello,控制台就会打印出问候语。嗯,就是这么简单。
小提示:Task 类必须声明为 open,因为 Gradle 会动态生成子类来实现一些内部机制。不加 open 的话,编译会报错。
6.3 @TaskAction 注解的秘密
@TaskAction 是自定义 Task 的核心。它标记的方法就是 Task 执行时真正运行的代码。
这里有几个要点:
- 只能有一个 @TaskAction 方法:多了 Gradle 不知道执行哪个
- 方法名随意:叫
greet()、run()、execute()都行 - 方法不能有参数:至少目前版本是这样
- 方法返回值必须是 void:或者返回 Unit
我曾经犯过一个错:在 @TaskAction 方法里传了个参数,结果 Gradle 直接报错说找不到合适的方法。后来查文档才发现,@TaskAction 方法必须是无参的。
6.4 给 Task 添加可配置属性
光有执行逻辑还不够,很多时候我们需要让 Task 接受外部配置。Gradle 提供了 @Input、@Output 等注解来实现:
open class GreetTask : DefaultTask() {
@Input
var greeting: String = "Hello"
@Input
var name: String = "World"
@OutputFile
var outputFile: File = project.layout.buildDirectory
.file("greeting.txt")
.get()
.asFile
@TaskAction
fun writeGreeting() {
val message = "$greeting, $name!"
outputFile.writeText(message)
println("Written: $message")
}
}
使用的时候:
tasks.register<GreetTask>("greet") {
greeting = "你好"
name = "Gradle"
outputFile = layout.buildDirectory.file("my-greeting.txt").get().asFile
}
注意:@Input 和 @Output 注解不只是装饰。它们告诉 Gradle 这个 Task 的输入输出是什么,从而实现增量构建。不加注解的话,每次都会重新执行,构建速度会变慢。
6.5 Task 配置避免(Configuration Avoidance)
这是 Gradle 5.1 引入的重要概念。说白了就是:不要提前配置你用不到的 Task。
传统写法是这样的:
// ❌ 不推荐:每次配置阶段都会创建并配置这个 Task
val myTask by tasks.creating(MyTask::class) {
inputFile = file("input.txt")
}
这种写法的问题在于:即使你最终没有执行 myTask,Gradle 在配置阶段也会创建它并执行配置代码。项目大了以后,配置阶段会越来越慢。
正确的做法是使用 Task 配置避免 API:
// ✅ 推荐:只有 Task 被需要时才创建和配置
val myTask by tasks.registering(MyTask::class) {
inputFile = file("input.txt")
}
tasks.registering 返回的是一个 TaskProvider,它是个懒加载的代理。只有当你真正执行这个 Task,或者显式引用了它的输出时,才会触发实际的创建和配置。
6.6 实战:一个文件处理 Task
结合上面讲的知识点,我们来写一个实用的 Task:把项目中的 Markdown 文件批量转换为 HTML。
open class MarkdownToHtmlTask : DefaultTask() {
@InputDirectory
var inputDir: File = project.projectDir
@OutputDirectory
var outputDir: File = project.layout.buildDirectory
.file("html")
.get()
.asFile
@Input
var fileExtension: String = ".md"
@TaskAction
fun convert() {
// 确保输出目录存在
outputDir.mkdirs()
// 查找所有 Markdown 文件
inputDir.walkTopDown()
.filter { it.extension == fileExtension.removePrefix(".") }
.forEach { mdFile ->
val relativePath = mdFile.relativeTo(inputDir)
val htmlFile = outputDir.resolve(
relativePath.path.replace(fileExtension, ".html")
)
htmlFile.parentFile.mkdirs()
// 简单的转换逻辑(实际项目可以用库)
val content = mdFile.readText()
val html = """
<!DOCTYPE html>
<html><body>
<pre>${content}</pre>
</body></html>
""".trimIndent()
htmlFile.writeText(html)
println("Converted: ${mdFile.name} -> ${htmlFile.name}")
}
}
}
在 build.gradle.kts 中注册:
tasks.register<MarkdownToHtmlTask>("convertDocs") {
inputDir = projectDir.resolve("docs")
outputDir = layout.buildDirectory.dir("generated-html").get().asFile
}
6.7 避坑指南
我这些年踩过的坑,分享给大家:
- 不要在 Task 构造器里做耗时操作:构造器是在配置阶段执行的,耗时操作会拖慢整个构建
- 不要直接使用
project对象:尽量通过@Input、@Output注解传递依赖 - 记得处理文件编码:跨平台时,文件读写最好指定 UTF-8
- Task 类要放在 buildSrc 里:这样所有子模块都能复用
我曾经在 Task 构造器里写了个网络请求,结果每次执行 gradle tasks 都要等 5 秒。后来改成懒加载,配置时间从 8 秒降到了 1 秒。嗯,配置避免真的很重要。
6.8 知识体系总览
下面这张图总结了自定义 Task 的核心知识点和它们之间的关系:
6.9 小结
自定义 Task 是 Gradle Kotlin DSL 进阶的必修课。掌握了它,你就能写出更优雅、更高效的构建脚本。
记住几个关键点:
- 继承
DefaultTask,用@TaskAction标记执行方法 - 用
@Input/@Output注解声明输入输出,支持增量构建 - 用
tasks.registering实现配置避免,别用tasks.creating - Task 类放在
buildSrc里,方便复用
好了,这一章就到这里。去试试自己写一个自定义 Task 吧,你会发现构建脚本的世界豁然开朗。