6、Task进阶:任务的类型(Copy, Delete, Exec等)、自定义任务类、增量构建与输入输出
好,咱们继续往下走。上一章我们把Task的基础玩明白了,这一章得加点硬货了。说白了,Gradle里的Task不只是跑个脚本那么简单,它内置了好几种现成的任务类型,能帮你省不少事。而且,当你需要重复使用某段逻辑时,自定义任务类就是你的好帮手。最后,增量构建——嗯,这可是提升构建速度的关键,我当年在项目里踩过坑,今天一并讲清楚。
6.1 内置任务类型:Copy、Delete、Exec
Gradle 提供了几个非常实用的内置任务类型。你想想看,每次都要手写 doLast 去复制文件、删除目录,多累啊。直接用现成的类型,代码更干净,意图也更明确。
6.1.1 Copy 任务
复制文件是构建中最常见的操作之一。我个人习惯用 Copy 类型,而不是写一堆 ant.copy 或者自己搞文件流。
tasks.register('copyResources', Copy) {
from 'src/main/resources'
into 'build/resources'
include '**/*.properties'
exclude '**/secret.properties'
}
这里 from 和 into 是必须的。include 和 exclude 用来过滤文件。我在项目中遇到过一个问题:如果不加 include,它会复制整个目录,包括一些临时文件,导致构建产物里多了些不该有的东西。所以,养成好习惯,明确指定要什么。
rename 方法。比如 rename '(.*)-debug\.jar', '$1-release.jar'。
6.1.2 Delete 任务
清理构建产物,用 Delete 类型最直接。
tasks.register('cleanBuild', Delete) {
delete 'build', 'dist'
}
它会递归删除你指定的目录或文件。注意,它不会报错如果文件不存在——这点很安全。我曾经在写CI脚本时,用 Delete 清理临时目录,结果发现它把整个项目目录删了……嗯,因为我把 delete projectDir 写进去了。所以,千万小心 delete 的路径,最好用相对路径或者 layout.buildDirectory。
delete '/' 或者 delete project.rootDir。Gradle 不会阻止你,但后果很严重。
6.1.3 Exec 任务
有时候你需要执行系统命令,比如调用一个外部工具。这时候 Exec 类型就派上用场了。
tasks.register('runShellScript', Exec) {
commandLine 'bash', '-c', 'echo "Hello from Gradle" > output.txt'
}
或者执行一个可执行文件:
tasks.register('runMyTool', Exec) {
workingDir 'tools'
commandLine './my-tool', '--input', 'data.csv'
}
这里要注意:commandLine 的第一个参数是可执行文件,后面是参数。如果你用 executable 和 args 分开写,效果一样。我个人更习惯用 commandLine,因为一眼就能看到完整的命令。
我曾经在Windows和Linux混用的项目里,用 Exec 执行 gradlew 命令,结果路径分隔符搞错了。后来我改用 commandLine 'gradlew', 'build',让系统自己去找可执行文件,问题就解决了。
6.2 自定义任务类
当你发现多个Task有相似的逻辑时,就该考虑自定义任务类了。说白了,就是把重复的代码抽出来,封装成一个类,然后在 build.gradle 里直接注册使用。
6.2.1 在 build.gradle 中定义
最简单的做法,直接在 build.gradle 里写一个类:
abstract class GreetTask extends DefaultTask {
@Input
abstract Property<String> getGreeting()
@TaskAction
void greet() {
println "Hello, ${greeting.get()}!"
}
}
tasks.register('sayHello', GreetTask) {
greeting = 'World'
}
这里我用了 abstract class 和 abstract Property,这是Gradle推荐的写法。为什么?因为Gradle会帮你处理延迟赋值和配置缓存。如果你用普通的 String 字段,反而容易出问题。
DefaultTask,用 @TaskAction 标记执行方法,用 @Input、@Output 等注解声明输入输出。
6.2.2 在 buildSrc 中定义
如果任务类比较复杂,或者要在多个项目间共享,我建议放到 buildSrc 目录下。这样所有子项目都能用,而且支持IDE的代码补全。
目录结构:
buildSrc/
src/
main/
groovy/
com/example/MyTask.groovy
java/
com/example/MyTask.java
然后你就可以在任意子项目的 build.gradle 里直接引用:
tasks.register('myTask', com.example.MyTask) {
// 配置属性
}
我个人习惯用Java写任务类,因为类型安全,而且Gradle本身也是Java写的。不过Groovy也可以,看团队喜好。
6.3 增量构建与输入输出
这是Gradle最强大的特性之一。说白了,就是只重新执行那些输入有变化的任务,没变化的直接跳过。这样构建速度能快很多。
6.3.1 输入和输出注解
Gradle通过注解来判断一个任务的输入和输出是什么。常见的注解有:
| 注解 | 说明 | 示例 |
|---|---|---|
@Input |
单个输入值 | @Input String version |
@InputFile |
单个输入文件 | @InputFile File config |
@InputDirectory |
输入目录 | @InputDirectory File srcDir |
@InputFiles |
多个输入文件 | @InputFiles FileCollection sources |
@OutputFile |
单个输出文件 | @OutputFile File output |
@OutputDirectory |
输出目录 | @OutputDirectory File destDir |
举个例子:
abstract class TransformTask extends DefaultTask {
@InputFile
abstract RegularFileProperty getInputFile()
@OutputFile
abstract RegularFileProperty getOutputFile()
@TaskAction
void transform() {
def input = inputFile.get().asFile
def output = outputFile.get().asFile
output.text = input.text.toUpperCase()
}
}
这样,只要 inputFile 没变,Gradle就会跳过这个任务。我第一次用的时候,感觉像开了挂一样——构建时间从30秒降到了2秒。
6.3.2 增量构建的原理
Gradle会为每个任务计算一个“输入快照”和“输出快照”。如果两次构建之间,输入和输出都没变,任务就被标记为 UP-TO-DATE。如果输入变了但输出没变,任务会被重新执行。如果输出被删了,也会重新执行。
为什么会这样?因为Gradle要保证结果的一致性。你想想看,如果输出文件被人手动删了,但输入没变,Gradle还是会重新生成输出,这样才合理。
@OutputFile 注解,结果每次构建都重新执行,因为Gradle认为这个任务没有输出,所以永远不是 UP-TO-DATE。记住:没有输出注解的任务,永远不会被跳过。
6.3.3 如何验证增量构建
你可以用 --info 日志级别来查看任务状态:
./gradlew build --info
输出中会看到类似:
:transform UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
如果看到 EXECUTED,说明任务被重新执行了。如果看到 FROM-CACHE,说明是从构建缓存里拿的结果。
6.4 知识体系图
下面这张图帮你理清本章的核心逻辑:
6.5 实战建议
好了,理论讲完了,说点实际的。我在做微服务构建时,经常需要把不同模块的配置文件复制到统一目录。这时候我会定义一个 Copy 任务,然后用 dependsOn 串联起来。如果配置没变,Gradle会自动跳过,省了不少时间。
另外,自定义任务类一定要放在 buildSrc 里。为什么?因为这样你可以在多个子项目里复用,而且IDE能给你代码提示。我曾经见过有人把任务类写在 build.gradle 里,结果每个子项目都要复制一遍,维护起来简直是噩梦。
最后,关于增量构建,我建议你养成习惯:每个自定义任务都加上 @Input 和 @Output 注解。哪怕现在只有一个输入,也加上。因为以后扩展时,你很可能忘了加,导致构建性能下降。
./gradlew build --dry-run 看看它会不会被标记为 UP-TO-DATE。
嗯,这一章的内容就到这里。记住:内置任务类型是基础,自定义任务类是进阶,增量构建是性能保障。三者结合,你的构建脚本会既干净又高效。
公众号:蓝海资料掘金营,微信deep3321