6、自定义Task:创建自定义Task类、使用@TaskAction注解、Task配置避免

说实话,Gradle 的 Task 系统是构建脚本的灵魂。但很多同学写脚本时,习惯把一堆逻辑塞进 doLastdoFirst 里。脚本一长,维护起来就头疼了。

我个人习惯是:只要逻辑稍微复杂一点,就抽成自定义 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 的核心知识点和它们之间的关系:

自定义 Task 知识体系 自定义 Task 类 继承 DefaultTask @TaskAction 注解 @Input / @Output Task 配置避免 tasks.registering() 增量构建支持 类型安全 · 可复用 · 可测试 · 高性能 核心思想:将 Task 的配置与执行分离,利用 Gradle 的懒加载机制提升构建性能

6.9 小结

自定义 Task 是 Gradle Kotlin DSL 进阶的必修课。掌握了它,你就能写出更优雅、更高效的构建脚本。

记住几个关键点:

  • 继承 DefaultTask,用 @TaskAction 标记执行方法
  • @Input/@Output 注解声明输入输出,支持增量构建
  • tasks.registering 实现配置避免,别用 tasks.creating
  • Task 类放在 buildSrc 里,方便复用

好了,这一章就到这里。去试试自己写一个自定义 Task 吧,你会发现构建脚本的世界豁然开朗。


公众号:蓝海资料掘金营,微信deep3321