4. Task详解:Task的创建与配置、Task依赖与排序、增量构建与缓存、Task类型与自定义Task

Task 是 Gradle 构建脚本里的基本执行单元。说白了,你写的每一个构建动作——编译、打包、测试、部署——最终都会映射成一个或多个 Task。我刚开始用 Gradle 时,总觉得 Task 不就是个任务嘛,跑就完了。后来在大型项目里踩了不少坑,才真正理解 Task 的设计哲学。

这一章,我会把 Task 的方方面面拆开来讲。从创建配置,到依赖排序,再到增量构建和自定义类型,咱们一步步来。

4.1 Task 的创建与配置

创建 Task 的方式有好几种。我个人最常用的是通过 tasks.register() 方法,这种方式是延迟创建的,性能更好。

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

// 方式二:使用 create(立即创建)
tasks.create('helloImmediate') {
    doLast {
        println 'Hello immediately!'
    }
}

// 方式三:通过 TaskContainer
tasks.register('greet') {
    description = '一个打招呼的任务'
    group = 'MyTasks'
    
    doLast {
        println "你好,${project.name}!"
    }
}

配置 Task 也很灵活。你可以在注册时直接配置,也可以事后通过 named() 来配置。

tasks.register('configurableTask') {
    // 注册时配置
    ext.customProp = '初始值'
}

// 事后配置
tasks.named('configurableTask') {
    ext.customProp = '修改后的值'
    doLast {
        println "属性值: ${customProp}"
    }
}

我的习惯: 能用 register 就别用 create。延迟创建在配置阶段开销小,尤其是项目里有上百个 Task 时,差距很明显。

4.2 Task 依赖与排序

Task 之间往往有先后关系。比如,测试前得先编译,打包前得先测试。Gradle 提供了两种方式来控制执行顺序:依赖排序

4.2.1 依赖(dependsOn)

依赖意味着一个 Task 必须在另一个 Task 之后执行。如果被依赖的 Task 失败了,当前 Task 也不会执行。

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

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

// 执行 taskB 时,会自动先执行 taskA

4.2.2 排序(mustRunAfter / shouldRunAfter)

排序和依赖不同。排序只是规定顺序,不强制要求执行。如果两个 Task 都被包含在执行列表中,排序规则才会生效。

tasks.register('taskX') {
    doLast { println 'Task X' }
}

tasks.register('taskY') {
    mustRunAfter 'taskX'  // 如果同时执行,taskY 在 taskX 之后
    doLast { println 'Task Y' }
}

// 执行 gradle taskY taskX,顺序是 taskX -> taskY

我曾经踩过的坑: 把 mustRunAfter 当 dependsOn 用。结果 taskX 没执行,taskY 却跑了,因为排序不触发执行。记住:排序只是排队,依赖才是叫号。

4.2.3 依赖与排序的对比

特性 dependsOn mustRunAfter shouldRunAfter
是否触发执行
失败时是否中断
强制执行 否(可被覆盖)
典型场景 编译后测试 清理后构建 日志输出顺序

4.3 增量构建与缓存

这是 Gradle 最让我佩服的地方。你想想看,如果每次构建都重新编译所有代码,那得多慢?增量构建就是为了解决这个问题。

4.3.1 增量构建的原理

Gradle 通过 输入(Inputs)输出(Outputs) 来判断 Task 是否需要重新执行。如果输入没变,输出也没变,那这个 Task 就可以跳过。

tasks.register('processData') {
    // 声明输入
    inputs.file('data/input.txt')
    // 声明输出
    outputs.file('build/output.txt')
    
    doLast {
        def input = file('data/input.txt').text
        file('build/output.txt').text = input.toUpperCase()
        println '数据处理完成'
    }
}

第一次执行时,Gradle 会记录输入文件的哈希值。第二次执行时,如果 input.txt 没变,Task 会被标记为 UP-TO-DATE,直接跳过。

4.3.2 缓存(Build Cache)

缓存比增量构建更进一步。增量构建只在本地有效,缓存可以跨机器共享。比如 CI 服务器构建了一次,开发人员可以直接拉取缓存结果。

// settings.gradle
buildCache {
    local {
        enabled = true
        directory = new File(rootDir, 'build-cache')
    }
    remote(HttpBuildCache) {
        url = 'https://cache.example.com/'
        push = true
    }
}

核心要点: 想让 Task 支持缓存,必须正确声明 inputs 和 outputs。少声明一个,缓存就可能失效。我在项目中见过有人漏了 inputs.files,结果每次构建都全量执行,白白浪费了时间。

4.3.3 如何让自定义 Task 支持增量

abstract class IncrementalTask extends DefaultTask {
    
    @InputFile
    abstract RegularFileProperty getInputFile()
    
    @OutputDirectory
    abstract DirectoryProperty getOutputDir()
    
    @TaskAction
    void process() {
        def input = inputFile.get().asFile.text
        def output = new File(outputDir.get().asFile, 'result.txt')
        output.text = input.reverse()
        logger.lifecycle('增量处理完成')
    }
}

tasks.register('reverseText', IncrementalTask) {
    inputFile = file('source.txt')
    outputDir = file('build/output')
}

嗯,这里要注意:@InputFile@OutputDirectory 这些注解是 Gradle 识别输入输出的关键。少了它们,增量构建就不起作用。

4.4 Task 类型与自定义 Task

Gradle 内置了很多 Task 类型,比如 CopyDeleteExecZip 等。但真实项目里,我们经常需要自定义 Task 类型。

4.4.1 内置 Task 类型的使用

tasks.register('backupConfig', Copy) {
    from 'src/main/resources/config'
    into 'build/backup'
    include '*.properties'
}

tasks.register('cleanBuild', Delete) {
    delete 'build', 'dist'
}

tasks.register('runScript', Exec) {
    commandLine 'python', 'scripts/deploy.py'
}

4.4.2 自定义 Task 类型

当内置类型不够用时,就该自己动手了。我建议用抽象类 + @TaskAction 注解的方式,这样最规范。

abstract class ReportTask extends DefaultTask {
    
    @Input
    abstract Property<String> getReportName()
    
    @InputFiles
    abstract ConfigurableFileCollection getSourceFiles()
    
    @OutputFile
    abstract RegularFileProperty getReportFile()
    
    @TaskAction
    void generateReport() {
        def report = new StringBuilder()
        report.appendLine("报告名称: ${reportName.get()}")
        report.appendLine("生成时间: ${new Date()}")
        
        sourceFiles.each { file ->
            report.appendLine("文件: ${file.name} - ${file.length()} bytes")
        }
        
        reportFile.get().asFile.text = report.toString()
        logger.lifecycle("报告已生成: ${reportFile.get()}")
    }
}

// 使用自定义 Task
tasks.register('generateSummary', ReportTask) {
    reportName = '项目摘要'
    sourceFiles.from(fileTree('src').include('**/*.java'))
    reportFile = file('build/reports/summary.txt')
}

我的建议: 自定义 Task 时,尽量用 PropertyRegularFileProperty 这类延迟属性。它们比直接赋值更灵活,也更容易支持增量构建。

4.4.3 自定义 Task 的最佳实践

  • 使用抽象类:Gradle 会为抽象属性生成实现,省去手动赋值的麻烦。
  • 声明所有输入输出:漏掉一个,增量构建就可能失效。
  • 用 @TaskAction 标记执行方法:别在构造函数里写逻辑。
  • 提供合理的默认值:让使用者少配置一些。
  • 添加 group 和 description:方便在 gradle tasks 中查看。

4.5 知识体系总览

下面这张图,我把 Task 的核心知识点串了起来。你可以把它当作一个快速索引。

Gradle Task 创建与配置 register / create named / configure 依赖与排序 dependsOn mustRunAfter 增量构建与缓存 inputs / outputs UP-TO-DATE / 缓存 类型与自定义 Copy / Delete / Exec @TaskAction 自定义 核心:正确声明输入输出,合理使用依赖与排序 让 Gradle 帮你做最聪明的构建决策

Task 是 Gradle 构建的基石。掌握了创建、依赖、增量和自定义这四块,你就能写出高效、可维护的构建脚本。我在项目中见过太多人把 Task 写成大泥球,几百行代码塞在一个 doLast 里。其实只要拆分成多个小 Task,加上合理的依赖和增量配置,构建速度能提升好几倍。

记住:好的构建脚本,不是功能多,而是该快的时候快,该省的时候省。


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