20、自定义任务类型:抽象Task类、@TaskAction注解、输入输出注解、增量任务实现

说实话,Gradle 里内置的任务类型虽然多,但真到了实际项目里,你总会遇到一些「它没给我准备好」的场景。比如我去年做一个微服务代码生成器,需要把模板文件批量渲染成 Java 类——这种活儿,Gradle 自带的 CopyExec 根本搞不定。

这时候你就得自己写任务类型了。别怕,Gradle 把这套机制设计得挺优雅的。你只需要继承 DefaultTask,加上几个注解,一个可复用的任务类型就出来了。

20.1 抽象Task类:你的任务模板

先看一个最简单的例子。我习惯把自定义任务类型放在 buildSrc 里,这样整个项目都能用。

// buildSrc/src/main/groovy/com/example/tasks/MyTask.groovy
abstract class MyTask extends DefaultTask {

    @Input
    abstract Property<String> getMessage()

    @TaskAction
    void doSomething() {
        println "Hello, ${message.get()}!"
    }
}

这里有几个关键点:

  • 继承 DefaultTask:这是所有自定义任务的基类。它帮你处理了任务依赖、生命周期这些底层逻辑。
  • 抽象属性:用 abstract Property<T> 声明输入。Gradle 会自动帮你生成 getter/setter,你不需要写实现。
  • @TaskAction:标记这个方法就是任务执行时的入口。一个任务可以有多个 @TaskAction 方法,但执行顺序不保证——我个人建议只用一个。
我的习惯:任务类名尽量用动词开头,比如 GenerateCodeTaskValidateConfigTask。这样在 build.gradle 里注册时,读起来像一句指令。

20.2 @TaskAction注解:任务执行的灵魂

你可能会问:为什么不用 doLastdoFirst?嗯,这两个是给脚本里临时加逻辑用的。如果你要定义一个可复用的任务类型,@TaskAction 才是正道。

它有几个特性:

  • 方法可以是 void,也可以返回 void——反正返回值会被忽略。
  • 方法可以抛出异常,Gradle 会帮你处理失败状态。
  • 方法可以接受 TaskExecutionException 等参数,但一般不需要。

我曾经踩过一个坑:在 @TaskAction 方法里调用了 project.file()。当时觉得没问题,结果任务在配置阶段就报错了。为什么?因为 @TaskAction 方法是在执行阶段运行的,但 project.file() 在配置阶段也能跑——可如果你在方法里用了 project 的某些 API,可能会触发配置阶段的副作用。后来我改成用 @InputFiles 注解来声明文件输入,问题就解决了。

注意@TaskAction 方法里不要直接操作 project 对象。所有输入输出都应该通过注解声明,这样 Gradle 才能做增量构建。

20.3 输入输出注解:让Gradle知道你在干什么

Gradle 的增量构建,说白了就是靠这些注解来「猜」你的任务需不需要重新执行。如果你不声明输入输出,Gradle 会认为这个任务每次都要跑——那构建速度就上不去了。

常用的注解有这些:

注解 用途 示例
@Input 单个输入值(字符串、数字等) @Input abstract Property<String> getEncoding()
@InputFile 单个输入文件 @InputFile abstract RegularFileProperty getConfigFile()
@InputFiles 多个输入文件(集合) @InputFiles abstract ConfigurableFileCollection getSourceFiles()
@OutputFile 单个输出文件 @OutputFile abstract RegularFileProperty getOutputFile()
@OutputDirectory 输出目录 @OutputDirectory abstract DirectoryProperty getOutputDir()
@Internal 内部状态,不影响增量判断 @Internal abstract Property<Boolean> getVerbose()

来看一个完整的例子:

abstract class MarkdownToHtmlTask extends DefaultTask {

    @InputFiles
    abstract ConfigurableFileCollection getMarkdownFiles()

    @OutputDirectory
    abstract DirectoryProperty getOutputDir()

    @Input
    abstract Property<String> getTemplate()

    @TaskAction
    void convert() {
        // 这里写转换逻辑
        outputDir.get().asFile.mkdirs()
        markdownFiles.each { file ->
            // 处理每个 .md 文件
        }
    }
}

你想想看,如果我不声明 @InputFiles,Gradle 就不知道这个任务依赖哪些文件。哪怕你只改了一个 .md 文件,Gradle 也会把所有文件重新处理一遍。声明了之后,它就能精确地只重新处理有变化的文件。

20.4 增量任务实现:让构建飞起来

增量任务,说白了就是「只处理有变化的部分」。Gradle 提供了两种方式:

  1. 基于输入输出的增量:只要输入输出没变,任务就不执行。这是最基础的。
  2. 基于文件变更的增量:任务执行时,你可以知道哪些文件新增了、修改了、删除了。这需要实现 IncrementalTaskInputs 接口。

第二种方式更精细,但写起来也复杂一点。我直接上代码:

abstract class IncrementalCopyTask extends DefaultTask {

    @Incremental
    @InputFiles
    abstract ConfigurableFileCollection getSourceFiles()

    @OutputDirectory
    abstract DirectoryProperty getOutputDir()

    @TaskAction
    void execute(IncrementalTaskInputs inputs) {
        if (!inputs.incremental) {
            // 如果无法增量,就全量处理
            project.delete(outputDir)
            // 复制所有文件
        } else {
            inputs.outOfDate { change ->
                // 处理新增或修改的文件
                println "处理: ${change.file.name}"
            }
            inputs.removed { change ->
                // 处理被删除的文件
                println "删除: ${change.file.name}"
            }
        }
    }
}
关键点@Incremental 注解告诉 Gradle 这个任务支持增量处理。方法参数里的 IncrementalTaskInputs 会给你提供变更信息。

我曾经在一个 CI 流水线里用过这个。项目有 2000 多个模板文件,每次构建都要全部渲染一遍,耗时 3 分钟。改成增量任务后,如果只改了一个文件,构建时间直接降到 5 秒。你想想看,这差距有多大。

20.5 实战:一个代码生成任务

最后,我分享一个我在项目中用过的真实案例。这个任务读取一个 JSON 配置文件,生成对应的 Java POJO 类。

abstract class GeneratePojoTask extends DefaultTask {

    @InputFile
    abstract RegularFileProperty getSchemaFile()

    @OutputDirectory
    abstract DirectoryProperty getOutputDir()

    @Input
    abstract Property<String> getPackageName()

    @TaskAction
    void generate() {
        def schema = new JsonSlurper().parse(schemaFile.get().asFile)
        outputDir.get().asFile.mkdirs()

        schema.each { className, fields ->
            def file = new File(outputDir.get().asFile, "${className}.java")
            file.text = buildPojo(className, fields)
        }
    }

    private String buildPojo(String name, Map fields) {
        // 生成 Java 代码的逻辑
    }
}

使用时,在 build.gradle 里注册:

tasks.register('generateUserPojo', GeneratePojoTask) {
    schemaFile = file('user-schema.json')
    outputDir = file('src/generated/java')
    packageName = 'com.example.model'
}
避坑指南:我曾经把 @InputFile 写成了 @InputFiles,结果 Gradle 认为输入是一个集合,导致增量判断失效。记住:单个文件用 @InputFile,多个文件用 @InputFiles

20.6 知识体系图

下面这张图帮你理清自定义任务类型的核心逻辑:

自定义任务类型核心结构 自定义任务 extends DefaultTask @TaskAction 注解 @Input / @Output 注解 @Incremental 增量 任务执行入口方法 声明输入输出,支持增量构建 只处理变更的文件 三者结合 → 可复用、高性能的自定义任务

这张图展示了自定义任务类型的三个核心要素:@TaskAction 定义执行逻辑,输入输出注解让 Gradle 理解任务依赖,增量注解则让构建效率大幅提升。三者缺一不可。


好了,关于自定义任务类型,核心内容就这些。你可能会觉得注解有点多,但用熟了就会发现,它们其实是在帮你「偷懒」——让 Gradle 替你管理那些繁琐的依赖和增量判断。下次遇到重复性的构建逻辑,不妨试试自己写一个任务类型。

一句话总结:继承 DefaultTask + @TaskAction + 输入输出注解 + @Incremental = 一个专业、高效的自定义任务。

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