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'
}

这里 frominto 是必须的。includeexclude 用来过滤文件。我在项目中遇到过一个问题:如果不加 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 的第一个参数是可执行文件,后面是参数。如果你用 executableargs 分开写,效果一样。我个人更习惯用 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 classabstract 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 知识体系图

下面这张图帮你理清本章的核心逻辑:

Task进阶知识体系 内置任务类型 Copy Delete Exec 自定义任务类 build.gradle内定义 buildSrc中定义 @TaskAction 增量构建 @Input / @Output UP-TO-DATE 构建缓存 核心原则 1. 优先使用内置任务类型,减少手写代码 2. 重复逻辑抽取为自定义任务类,放在buildSrc中共享 3. 正确声明输入输出,让增量构建自动生效

6.5 实战建议

好了,理论讲完了,说点实际的。我在做微服务构建时,经常需要把不同模块的配置文件复制到统一目录。这时候我会定义一个 Copy 任务,然后用 dependsOn 串联起来。如果配置没变,Gradle会自动跳过,省了不少时间。

另外,自定义任务类一定要放在 buildSrc 里。为什么?因为这样你可以在多个子项目里复用,而且IDE能给你代码提示。我曾经见过有人把任务类写在 build.gradle 里,结果每个子项目都要复制一遍,维护起来简直是噩梦。

最后,关于增量构建,我建议你养成习惯:每个自定义任务都加上 @Input@Output 注解。哪怕现在只有一个输入,也加上。因为以后扩展时,你很可能忘了加,导致构建性能下降。

小提示: 如果你不确定某个任务是否支持增量构建,可以运行 ./gradlew build --dry-run 看看它会不会被标记为 UP-TO-DATE

嗯,这一章的内容就到这里。记住:内置任务类型是基础,自定义任务类是进阶,增量构建是性能保障。三者结合,你的构建脚本会既干净又高效。


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