25、自定义任务类型:继承DefaultTask、@TaskAction注解、@Input/@Output注解
说实话,Gradle 内置的任务类型虽然多,但真到了实际项目里,你总会遇到一些「它没帮我准备好」的场景。比如我最近做一个代码生成器插件,需要把模板文件批量处理成 Java 源码——这种活儿,用 Copy 或 Exec 都不太对味。
这时候就该我们自己动手了。自定义任务类型,说白了就是写一个普通的 Groovy 或 Java 类,继承 DefaultTask,然后告诉 Gradle:「嘿,这个类就是一个任务」。嗯,就这么简单。
继承 DefaultTask:任务的骨架
每个自定义任务类型,都得从 DefaultTask 开始。它就像一张白纸,给你提供了任务的生命周期管理、日志输出、依赖处理这些基础设施。
// build.gradle
class MyCustomTask extends DefaultTask {
// 这里写你的逻辑
}
你看,就这么几行,一个任务类型就定义好了。不过它现在还是个空壳子,啥也干不了。我们需要给它加点「动作」。
buildSrc 目录下,或者单独抽成一个插件项目。直接写在 build.gradle 里虽然方便,但项目一复杂就不好维护了。
@TaskAction 注解:任务的灵魂
一个任务总得干点啥吧?@TaskAction 就是用来标记「干活的方法」的。Gradle 在执行这个任务时,会自动调用被这个注解标记的方法。
class MyCustomTask extends DefaultTask {
@TaskAction
void doWork() {
println "Hello from my custom task!"
// 这里写你的业务逻辑
}
}
为什么会用注解而不是接口?我个人理解是,Gradle 想给你更多灵活性——你可以定义多个辅助方法,只有被 @TaskAction 标记的那个才是入口。我曾经见过一个同事把整个任务的逻辑塞在一个方法里,结果调试起来特别痛苦。我的建议是:把核心逻辑拆成几个小方法,然后在 @TaskAction 方法里按顺序调用它们。
@Input / @Output 注解:让任务可配置、可增量
光能干活还不够,任务还得知道「对谁干」和「干完的结果放哪」。这就是 @Input 和 @Output 的用武之地。
| 注解 | 作用 | 示例 |
|---|---|---|
| @Input | 标记输入文件、目录或属性 | @Input File sourceDir |
| @InputFile | 标记单个输入文件 | @InputFile File configFile |
| @InputDirectory | 标记输入目录 | @InputDirectory File templateDir |
| @Output | 标记输出文件或目录 | @Output File outputDir |
| @OutputFile | 标记单个输出文件 | @OutputFile File resultFile |
| @OutputDirectory | 标记输出目录 | @OutputDirectory File genDir |
有了这些注解,Gradle 就能做一件很聪明的事——增量构建。说白了就是:如果输入没变,输出也没变,那这次任务就直接跳过,不执行。你想想看,一个项目几百个任务,每次只改了一个文件,要是全量执行得多浪费时间。
class CodeGeneratorTask extends DefaultTask {
@InputDirectory
File templateDir
@OutputDirectory
File outputDir
@Input
String encoding = "UTF-8"
@TaskAction
void generate() {
// 遍历 templateDir 下的所有模板文件
templateDir.eachFile { template ->
// 处理模板,生成到 outputDir
println "Processing ${template.name}..."
}
}
}
@OutputDirectory。结果每次构建都重新执行任务,因为 Gradle 认为「没有输出,所以永远需要执行」。加上注解后,增量构建就正常了。
知识体系:一张图看懂自定义任务
下面这张图梳理了自定义任务类型的核心要素和它们之间的关系。你可以把它当作一个速查地图。
实战:一个完整的自定义任务
光说不练假把式。我们写一个实际能用的任务:把指定目录下的 .txt 文件合并成一个文件。
class FileMergerTask extends DefaultTask {
@InputDirectory
File sourceDir
@OutputFile
File mergedFile
@Input
String separator = "\n---\n"
@TaskAction
void merge() {
// 确保输出目录存在
mergedFile.parentFile.mkdirs()
// 收集所有 .txt 文件
def txtFiles = sourceDir.listFiles().findAll { it.name.endsWith('.txt') }
// 按文件名排序,保证顺序一致
txtFiles.sort { it.name }
// 写入合并结果
mergedFile.withWriter { writer ->
txtFiles.eachWithIndex { file, index ->
if (index > 0) {
writer.write(separator)
}
writer.write(file.text)
}
}
println "Merged ${txtFiles.size()} files into ${mergedFile.name}"
}
}
// 注册并使用
task mergeTextFiles(type: FileMergerTask) {
sourceDir = file('src/texts')
mergedFile = file('build/merged.txt')
separator = "\n=====\n"
}
- 继承
DefaultTask获得任务基础设施 @TaskAction标记的方法就是任务执行时的入口@Input和@Output系列注解让 Gradle 能判断任务是否需要重新执行- 输入输出注解是增量构建的关键——不加的话每次都会全量执行
嗯,到这里你应该已经掌握了自定义任务的核心套路。说白了就是三步:继承、注解、写逻辑。我在实际项目中用这个模式写过代码格式化、接口文档生成、数据库迁移脚本……几乎能想到的自动化场景都能套用。
对了,还有一点要提醒:任务类里的属性最好用 File 或 String 类型,别用 def。Gradle 的注解处理器对具体类型支持更好,用 def 可能会导致增量构建失效。这个坑我替你们踩过了。