5、Task详解:Task的创建与配置、Task依赖与排序、Task类型、自定义Task类

Task 是 Gradle 构建的核心执行单元。说白了,构建脚本里写的那些「干活」的代码,最终都要封装成 Task 来运行。我刚开始用 Gradle 时,总觉得 Task 就是个简单的任务定义,后来踩了不少坑才明白——Task 的创建、配置、依赖关系,以及如何自定义 Task 类型,这些才是构建自动化的基本功。

这一章,咱们就把 Task 彻底讲透。你想想看,如果连 Task 都玩不转,那构建脚本写得再花哨,也只是空中楼阁。

Task 知识体系 Task 创建与配置 依赖与排序 内置 Task 类型 自定义 Task 类 task() 方法 配置块 dependsOn mustRunAfter extends DefaultTask @TaskAction

5.1 Task 的创建与配置

创建 Task 的方式有好几种。我个人最常用的是 tasks.register() 方法,尤其是在 Gradle 4.9 之后,这种方式成了推荐做法。

// 方式一:使用 register(推荐)
tasks.register('hello') {
    doLast {
        println 'Hello, Gradle!'
    }
}

// 方式二:使用 create(传统方式)
tasks.create('helloOld') {
    doLast {
        println 'Hello from old way!'
    }
}

这里有个关键区别:register 是延迟创建的,Task 只有在真正需要时才会被实例化。而 create 会立即创建。我在项目中遇到过因为用了 create 导致配置阶段变慢的情况,后来全部改成了 register,构建速度提升了不少。

配置 Task 有两种时机:

  • 配置时配置:在 Task 创建时直接写配置逻辑
  • 执行时配置:使用 doFirst / doLast 添加动作
tasks.register('configDemo') {
    // 配置时执行
    description = '这是一个配置示例'
    
    // 执行时执行
    doFirst {
        println '任务开始前执行'
    }
    doLast {
        println '任务结束后执行'
    }
}
💡 我的习惯:把 Task 的「属性配置」放在配置块里,把「业务逻辑」放在 doLast 里。这样职责清晰,调试也方便。

5.2 Task 依赖与排序

Task 之间的执行顺序,说白了就两种关系:依赖排序

依赖(dependsOn):A 依赖 B,那么 B 必须在 A 之前执行。如果 B 失败了,A 也不会执行。

排序(mustRunAfter / shouldRunAfter):只是指定顺序,不强制依赖关系。A 排在 B 后面,但如果 A 和 B 没有依赖关系,它们可能根本不会同时出现。

tasks.register('taskA') {
    doLast { println '执行 A' }
}

tasks.register('taskB') {
    dependsOn taskA  // B 依赖 A
    doLast { println '执行 B' }
}

tasks.register('taskC') {
    mustRunAfter taskA  // C 排在 A 后面
    doLast { println '执行 C' }
}

我曾经犯过一个错误:用 dependsOn 来强行排序两个没有逻辑依赖的任务。结果导致构建图变得很复杂,并行执行时反而更慢了。后来改用 mustRunAfter,问题就解决了。

⚠️ 注意mustRunAftershouldRunAfter 的区别在于——前者是强约束,如果出现循环排序会报错;后者是弱约束,Gradle 在无法满足时会忽略它。

5.3 Task 类型

Gradle 内置了很多实用的 Task 类型。你不需要每次都从零开始写。我整理了几个最常用的:

Task 类型 用途 典型场景
Copy 复制文件 将资源文件复制到输出目录
Delete 删除文件或目录 清理构建产物
Exec 执行外部命令 调用系统工具或脚本
Zip / Tar 打包压缩 生成发布包
JavaExec 运行 Java 程序 启动应用或测试
// 使用 Copy 类型
tasks.register('copyResources', Copy) {
    from 'src/main/resources'
    into 'build/resources'
    include '**/*.properties'
}

// 使用 Exec 类型
tasks.register('runScript', Exec) {
    commandLine 'bash', '-c', 'echo "Hello from shell"'
}

// 使用 JavaExec 类型
tasks.register('runApp', JavaExec) {
    mainClass = 'com.example.Main'
    classpath = sourceSets.main.runtimeClasspath
}

核心要点:使用内置 Task 类型时,你只需要配置它的「属性」,不需要写 doLast。因为这些类型的执行逻辑已经封装好了。比如 Copyfrominto 就是它的配置属性。

5.4 自定义 Task 类

当内置类型不够用时,就该自己写 Task 类了。嗯,这里要注意:自定义 Task 类不是写一个闭包就完事了,而是要创建一个真正的类。

// 在 buildSrc 或项目中定义
abstract class MyTask extends DefaultTask {
    
    @Input
    abstract Property<String> getMessage()
    
    @OutputFile
    abstract RegularFileProperty getOutputFile()
    
    @TaskAction
    void doWork() {
        def msg = message.get()
        def file = outputFile.get().asFile
        file.text = "Message: $msg"
        logger.quiet "已写入文件: $file"
    }
}

// 使用自定义 Task
tasks.register('myTask', MyTask) {
    message = 'Hello from custom task!'
    outputFile = layout.buildDirectory.file('output.txt')
}

这里有几个关键点:

  • 继承 DefaultTask:这是所有自定义 Task 的基类
  • 使用 @TaskAction:标记执行方法,Gradle 会自动调用
  • 使用 @Input / @OutputFile:声明输入输出,这是增量构建的基础
  • 使用 Property 类型:而不是普通的 StringFile,这样才能支持延迟配置
⚠️ 避坑指南:我曾经在自定义 Task 里直接用了 String message 而不是 Property<String>。结果在配置时一切正常,但到了执行时,message 的值总是 null。原因就是没有使用延迟属性。记住:在 Gradle 的 Task 类里,所有可配置的属性都应该用 Property 类型

另外,自定义 Task 类最好放在 buildSrc 目录下,这样整个项目都能复用。如果只是单个脚本用,也可以直接写在 build.gradle 里,但我不建议这么做——代码会变得很臃肿。

💡 我的建议:刚开始写自定义 Task 时,先从一个简单的 @TaskAction 方法开始。等跑通了,再逐步添加 @Input@Output 等注解。别一上来就想搞个大而全的 Task,容易把自己绕进去。

好了,Task 的核心内容就这些。从创建配置,到依赖排序,再到内置类型和自定义类——每一步都有它的设计哲学。你想想看,Gradle 之所以强大,很大程度上就是因为它把「任务」这个抽象概念做得足够灵活。掌握了 Task,你就掌握了 Gradle 构建的「原子操作」。


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