20、自定义任务类型:抽象Task类、@TaskAction注解、输入输出注解、增量任务实现
说实话,Gradle 里内置的任务类型虽然多,但真到了实际项目里,你总会遇到一些「它没给我准备好」的场景。比如我去年做一个微服务代码生成器,需要把模板文件批量渲染成 Java 类——这种活儿,Gradle 自带的 Copy 或 Exec 根本搞不定。
这时候你就得自己写任务类型了。别怕,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方法,但执行顺序不保证——我个人建议只用一个。
GenerateCodeTask、ValidateConfigTask。这样在 build.gradle 里注册时,读起来像一句指令。
20.2 @TaskAction注解:任务执行的灵魂
你可能会问:为什么不用 doLast 或 doFirst?嗯,这两个是给脚本里临时加逻辑用的。如果你要定义一个可复用的任务类型,@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 提供了两种方式:
- 基于输入输出的增量:只要输入输出没变,任务就不执行。这是最基础的。
- 基于文件变更的增量:任务执行时,你可以知道哪些文件新增了、修改了、删除了。这需要实现
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 知识体系图
下面这张图帮你理清自定义任务类型的核心逻辑:
这张图展示了自定义任务类型的三个核心要素:@TaskAction 定义执行逻辑,输入输出注解让 Gradle 理解任务依赖,增量注解则让构建效率大幅提升。三者缺一不可。
好了,关于自定义任务类型,核心内容就这些。你可能会觉得注解有点多,但用熟了就会发现,它们其实是在帮你「偷懒」——让 Gradle 替你管理那些繁琐的依赖和增量判断。下次遇到重复性的构建逻辑,不妨试试自己写一个任务类型。
公众号:蓝海资料掘金营,微信deep3321