Skip to content

自定义 Task

源:Gradle 官方文档 - Authoring Tasks | Incremental Build

自定义任务让你能够扩展 Gradle 的功能,实现项目特定的构建逻辑。

自定义任务类的存放位置

位置选择

根据任务的复用范围,有三种存放位置:

直接在 build.gradle.kts 中 简单任务

app/
└── build.gradle.kts  ← 在这里定义任务类

适用场景

  • 仅在单个模块使用
  • 简单的一次性任务
  • 快速原型验证

buildSrc 目录 推荐

project/
├── buildSrc/
│   ├── build.gradle.kts
│   └── src/main/kotlin/
│       └── tasks/
│           └── MyCustomTask.kt  ← 在这里定义任务类
├── app/
│   └── build.gradle.kts  ← 在这里使用任务
└── core/
    └── build.gradle.kts  ← 在这里也可以使用

适用场景

  • 多个模块共享
  • 复杂的任务逻辑
  • 需要类型安全
  • 推荐的标准做法

独立插件项目

workspace/
├── my-plugin/
│   ├── build.gradle.kts
│   └── src/main/kotlin/
│       └── MyCustomTask.kt  ← 在这里定义
└── my-app/
    ├── settings.gradle.kts  ← includeBuild("../my-plugin")
    └── build.gradle.kts     ← 使用插件

适用场景

  • 跨项目共享
  • 发布到 Maven/Gradle Plugin Portal
  • 企业内部插件库

详细说明

方式一:直接在 build.gradle.kts

kotlin
// app/build.gradle.kts
abstract class SimpleTask : DefaultTask() {
    @TaskAction
    fun execute() {
        println("Simple task")
    }
}

tasks.register<SimpleTask>("simple")

方式二:buildSrc 目录

创建 buildSrc

kotlin
// buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    google()
    mavenCentral()
}

定义任务类

kotlin
// buildSrc/src/main/kotlin/tasks/CustomTasks.kt
package tasks

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

abstract class MyCustomTask : DefaultTask() {
    @TaskAction
    fun execute() {
        println("Custom task from buildSrc")
    }
}

使用

kotlin
// app/build.gradle.kts
import tasks.MyCustomTask

tasks.register<MyCustomTask>("myTask")

方式三:独立插件

插件项目

kotlin
// my-plugin/build.gradle.kts
plugins {
    `kotlin-dsl`
    `maven-publish`
}

gradlePlugin {
    plugins {
        create("myPlugin") {
            id = "com.example.my-plugin"
            implementationClass = "com.example.MyPlugin"
        }
    }
}
kotlin
// my-plugin/src/main/kotlin/com/example/MyPlugin.kt
class MyPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        target.tasks.register<MyCustomTask>("myTask")
    }
}

使用

kotlin
// settings.gradle.kts
includeBuild("../my-plugin")
kotlin
// build.gradle.kts
plugins {
    id("com.example.my-plugin")
}

创建自定义任务

Ad-hoc 任务 简单场景

基本语法

kotlin
tasks.register("myTask") {
    doLast {
        println("执行任务")
    }
}

带属性

kotlin
tasks.register("greet") {
    val name = "Gradle"
    
    doLast {
        println("Hello, $name!")
    }
}

任务类 推荐

定义任务类

kotlin
abstract class GreetTask : DefaultTask() {
    @get:Input
    abstract val greeting: Property<String>
    
    @TaskAction
    fun greet() {
        println(greeting.get())
    }
}

注册任务

kotlin
tasks.register<GreetTask>("greet") {
    greeting.set("Hello, Gradle!")
}

任务属性

为什么需要声明输入输出

在 Gradle 中,任务的输入和输出必须显式声明,这样 Gradle 才能:

实现增量构建

  • 输入未改变 → 任务跳过(UP-TO-DATE)
  • 仅输入改变 → 任务重新执行
  • 节省大量构建时间

支持构建缓存

  • 相同输入 → 复用缓存结果
  • 跨机器共享缓存
  • CI 和本地共享构建产物

任务依赖追踪

  • 自动建立任务依赖关系
  • A 任务的输出 = B 任务的输入 → 自动依赖
  • 无需手动 dependsOn

@Input 输入属性

作用:标记简单值类型的输入(字符串、数字、布尔值等)

为什么需要

  • Gradle 需要知道哪些值会影响任务结果
  • 值改变时重新执行任务
  • 值不变时跳过任务

适用场景

  • 版本号
  • 配置开关
  • 简单参数
kotlin
abstract class ProcessTask : DefaultTask() {
    @get:Input
    abstract val message: Property<String>
    
    @get:Input
    @get:Optional  // 可选输入不提供也不会报错
    abstract val prefix: Property<String>
    
    @TaskAction
    fun process() {
        val prefixValue = prefix.getOrElse("")
        println("$prefixValue${message.get()}")
    }
}

使用示例

kotlin
tasks.register<ProcessTask>("process") {
    message.set("Hello, Gradle!")
    prefix.set(">>> ")  // 可选,可以不设置
}

效果

  • 首次运行:执行任务
  • message 不变:跳过任务(UP-TO-DATE)
  • message 改变:重新执行

@InputFile 输入文件

作用:标记单个文件输入

为什么需要

  • Gradle 监控文件内容变化
  • 文件内容改变 → 重新执行
  • 文件未改变 → 跳过任务

适用场景

  • 配置文件
  • 模板文件
  • 数据文件
kotlin
abstract class ReadFileTask : DefaultTask() {
    @get:InputFile
    abstract val inputFile: RegularFileProperty
    
    @TaskAction
    fun read() {
        val content = inputFile.get().asFile.readText()
        println(content)
    }
}

tasks.register<ReadFileTask>("readFile") {
    inputFile.set(file("input.txt"))
}

工作原理

  • Gradle 计算文件的校验和(checksum)
  • 校验和不变 → 任务跳过
  • 校验和改变 → 任务执行

@InputFiles 多个文件

作用:标记多个文件输入(文件集合)

为什么需要

  • 处理一批文件
  • 任何文件改变都触发重新执行

适用场景

  • 源代码文件
  • 资源文件
  • 配置文件集合
kotlin
abstract class ProcessFilesTask : DefaultTask() {
    @get:InputFiles
    abstract val sources: ConfigurableFileCollection
    
    @TaskAction
    fun process() {
        sources.forEach { file ->
            println("Processing: ${file.name}")
        }
    }
}

tasks.register<ProcessFilesTask>("processFiles") {
    sources.from("src/main/resources")
    // 或者
    sources.from(fileTree("src") {
        include("**/*.txt")
    })
}

@InputDirectory 输入目录

作用:标记整个目录作为输入

为什么需要

  • 监控目录下所有文件
  • 目录内任何文件改变都触发执行
  • 适合处理整个目录的场景

适用场景

  • 资源目录
  • 源码目录
  • 文档目录
kotlin
abstract class ScanDirectoryTask : DefaultTask() {
    @get:InputDirectory
    abstract val sourceDir: DirectoryProperty
    
    @TaskAction
    fun scan() {
        sourceDir.get().asFile.walk().forEach {
            if (it.isFile) {
                println(it.name)
            }
        }
    }
}

tasks.register<ScanDirectoryTask>("scan") {
    sourceDir.set(file("src/main/java"))
}

输出属性

为什么需要声明输出

声明输出让 Gradle 能够:

管理构建产物

  • 知道任务会生成什么文件
  • 清理输出目录(clean 任务)
  • 避免冲突

支持缓存

  • 缓存输出文件
  • 下次直接使用缓存

任务链接

  • A 的输出 → B 的输入
  • 自动建立依赖

@OutputFile 输出文件

作用:标记任务会生成单个文件

为什么需要

  • Gradle 管理输出文件
  • 支持增量构建
  • 支持缓存

适用场景

  • 生成报告文件
  • 编译产物
  • 转换后的文件
kotlin
abstract class GenerateFileTask : DefaultTask() {
    @get:OutputFile
    abstract val outputFile: RegularFileProperty
    
    @TaskAction
    fun generate() {
        val content = "Generated at ${System.currentTimeMillis()}"
        outputFile.get().asFile.writeText(content)
    }
}

tasks.register<GenerateFileTask>("generateFile") {
    outputFile.set(layout.buildDirectory.file("generated.txt"))
}

工作原理

  • 任务执行前:Gradle 检查输出文件是否存在
  • 任务执行后:Gradle 记录输出文件状态
  • 下次构建:如果输入不变,直接使用缓存的输出

@OutputDirectory 输出目录

作用:标记任务会生成整个目录

为什么需要

  • 管理多个输出文件
  • 清理旧的输出
  • 支持缓存

适用场景

  • 生成多个报告
  • 编译输出目录
  • 批量处理结果
kotlin
abstract class GenerateReportTask : DefaultTask() {
    @get:OutputDirectory
    abstract val reportDir: DirectoryProperty
    
    @TaskAction
    fun generate() {
        val dir = reportDir.get().asFile
        dir.mkdirs()  // 创建目录
        
        // 生成多个文件
        File(dir, "report.html").writeText("<html>Report</html>")
        File(dir, "data.json").writeText("{}")
    }
}

tasks.register<GenerateReportTask>("generateReport") {
    reportDir.set(layout.buildDirectory.dir("reports"))
}

增量构建支持

什么是增量构建

概念:只重新执行输入改变的任务,跳过输入未改变的任务。

为什么重要

  • 节省时间:只做必要的工作
  • 提升效率:大项目中尤其明显
  • 改善体验:快速反馈

增量构建如何工作

Gradle 的检查流程

  1. 检查输入:计算所有 @Input/@InputFile 的值/校验和
  2. 检查输出:检查 @OutputFile/@OutputDirectory 是否存在
  3. 决策
    • 输入未改变 + 输出存在 → 跳过(UP-TO-DATE)
    • 输入改变 或 输出不存在 → 执行

基本增量任务

kotlin
abstract class TransformTask : DefaultTask() {
    @get:InputFile
    abstract val inputFile: RegularFileProperty
    
    @get:OutputFile
    abstract val outputFile: RegularFileProperty
    
    @TaskAction
    fun transform() {
        // 读取输入
        val input = inputFile.get().asFile.readText()
        
        // 转换
        val output = input.uppercase()
        
        // 写入输出
        outputFile.get().asFile.writeText(output)
    }
}

tasks.register<TransformTask>("transform") {
    inputFile.set(file("input.txt"))
    outputFile.set(file("output.txt"))
}

运行效果

bash
# 首次运行
$ ./gradlew transform
> Task :transform
BUILD SUCCESSFUL

# 再次运行(输入未改变)
$ ./gradlew transform
> Task :transform UP-TO-DATE
BUILD SUCCESSFUL

# 修改 input.txt 后运行
$ ./gradlew transform
> Task :transform
BUILD SUCCESSFUL

@SkipWhenEmpty

作用:当输入文件为空时跳过任务

为什么需要

  • 避免无意义的执行
  • 没有输入就没有输出

适用场景

  • 源文件处理
  • 资源编译
  • 可选的任务
kotlin
abstract class ProcessSourcesTask : DefaultTask() {
    @get:InputFiles
    @get:SkipWhenEmpty  // 没有源文件时跳过
    abstract val sources: ConfigurableFileCollection
    
    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty
    
    @TaskAction
    fun process() {
        sources.forEach { source ->
            // 处理源文件
            println("Processing: ${source.name}")
        }
    }
}

效果

  • sources 为空 → 任务跳过(SKIPPED)
  • sources 有文件 → 任务执行

@PathSensitive

作用:控制路径变化如何影响增量构建

为什么需要

  • 有时文件移动不应触发重新构建
  • 有时只关心文件名不关心路径

PathSensitivity 选项

选项说明使用场景
ABSOLUTE绝对路径改变触发很少使用
RELATIVE相对路径改变触发推荐:源代码
NAME_ONLY仅文件名改变触发配置文件
NONE忽略路径,仅内容资源文件
kotlin
abstract class CompileTask : DefaultTask() {
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)  // 相对路径敏感
    abstract val sources: ConfigurableFileCollection
    
    @TaskAction
    fun compile() {
        // Java 编译依赖包名,需要相对路径
        sources.forEach { println("Compiling: $it") }
    }
}

示例场景

kotlin
// 场景1:编译任务 - 使用 RELATIVE
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val javaSources: ConfigurableFileCollection

// 场景2:配置文件 - 使用 NAME_ONLY
@get:InputFile
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val configFile: RegularFileProperty

// 场景3:资源文件 - 使用 NONE
@get:InputFiles
@get:PathSensitive(PathSensitivity.NONE)
abstract val images: ConfigurableFileCollection

构建缓存支持

什么是构建缓存

概念:保存任务的输出,相同输入时直接使用缓存。

为什么重要

  • 跨分支共享:切换分支无需重新构建
  • 团队共享:CI 生成的缓存本地可用
  • 极大提升速度:几分钟变几秒

@CacheableTask

作用:标记任务可以使用构建缓存

kotlin
@CacheableTask
abstract class ProcessDataTask : DefaultTask() {
    @get:InputFile
    @get:PathSensitive(PathSensitivity.NONE)
    abstract val inputData: RegularFileProperty
    
    @get:OutputFile
    abstract val result: RegularFileProperty
    
    @TaskAction
    fun process() {
        // 处理数据
    }
}

启用缓存

properties
# gradle.properties
org.gradle.caching=true

Provider API 集成

使用 Provider

kotlin
abstract class ConfigurableTask : DefaultTask() {
    @get:Input
    abstract val version: Property<String>
    
    @TaskAction
    fun execute() {
        println("Version: ${version.get()}")
    }
}

tasks.register<ConfigurableTask>("printVersion") {
    version.set(project.version.toString())
}

延迟计算

kotlin
val gitHash = providers.exec {
    commandLine("git", "rev-parse", "HEAD")
}.standardOutput.asText.map { it.trim() }

tasks.register<GenerateTask>("generate") {
    hash.set(gitHash)
}

任务验证

@Internal 内部属性

kotlin
abstract class BuildTask : DefaultTask() {
    @get:Internal
    abstract val tempDir: DirectoryProperty
    
    @get:InputFile
    abstract val source: RegularFileProperty
    
    @get:OutputFile
    abstract val output: RegularFileProperty
}

@Console 控制台属性

kotlin
abstract class InteractiveTask : DefaultTask() {
    @get:Console
    abstract val verbose: Property<Boolean>
    
    @TaskAction
    fun execute() {
        if (verbose.get()) {
            println("Verbose output...")
        }
    }
}

实战案例

案例1:代码生成任务

kotlin
@CacheableTask
abstract class GenerateCodeTask : DefaultTask() {
    @get:InputFile
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val templateFile: RegularFileProperty
    
    @get:Input
    abstract val className: Property<String>
    
    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty
    
    @TaskAction
    fun generate() {
        val template = templateFile.get().asFile.readText()
        val code = template.replace("{{CLASS_NAME}}", className.get())
        
        val outputFile = File(outputDir.get().asFile, "${className.get()}.kt")
        outputFile.writeText(code)
    }
}

tasks.register<GenerateCodeTask>("generateCode") {
    templateFile.set(file("templates/Class.kt.template"))
    className.set("MyGeneratedClass")
    outputDir.set(layout.buildDirectory.dir("generated/kotlin"))
}

案例2:资源打包任务

kotlin
@CacheableTask
abstract class PackageResourcesTask : DefaultTask() {
    @get:InputDirectory
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val resourcesDir: DirectoryProperty
    
    @get:OutputFile
    abstract val outputZip: RegularFileProperty
    
    @TaskAction
    fun pack() {
        val resources = resourcesDir.get().asFile
        val output = outputZip.get().asFile
        
        ZipOutputStream(output.outputStream()).use { zip ->
            resources.walk().forEach { file ->
                if (file.isFile) {
                    val entry = ZipEntry(file.relativeTo(resources).path)
                    zip.putNextEntry(entry)
                    file.inputStream().use { it.copyTo(zip) }
                    zip.closeEntry()
                }
            }
        }
    }
}

案例3:版本信息任务

kotlin
abstract class GenerateVersionTask : DefaultTask() {
    @get:Input
    abstract val versionName: Property<String>
    
    @get:Input
    abstract val versionCode: Property<Int>
    
    @get:Input
    val gitHash: Provider<String> = providers.exec {
        commandLine("git", "rev-parse", "--short", "HEAD")
    }.standardOutput.asText.map { it.trim() }
    
    @get:OutputFile
    abstract val outputFile: RegularFileProperty
    
    @TaskAction
    fun generate() {
        val content = """
            Version Name: ${versionName.get()}
            Version Code: ${versionCode.get()}
            Git Hash: ${gitHash.get()}
            Build Time: ${java.time.Instant.now()}
        """.trimIndent()
        
        outputFile.get().asFile.writeText(content)
    }
}

tasks.register<GenerateVersionTask>("generateVersion") {
    versionName.set(project.version.toString())
    versionCode.set(1)
    outputFile.set(layout.buildDirectory.file("version.txt"))
}

任务组织

在 buildSrc 中定义

kotlin
// buildSrc/src/main/kotlin/tasks/CustomTasks.kt
abstract class MyCustomTask : DefaultTask() {
    @get:InputFile
    abstract val input: RegularFileProperty
    
    @get:OutputFile
    abstract val output: RegularFileProperty
    
    @TaskAction
    fun execute() {
        // 任务逻辑
    }
}

使用

kotlin
// app/build.gradle.kts
tasks.register<MyCustomTask>("myTask") {
    input.set(file("input.txt"))
    output.set(file("output.txt"))
}

约定插件

kotlin
// buildSrc/src/main/kotlin/MyPlugin.kt
class MyPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        target.tasks.register<MyCustomTask>("myTask") {
            // 默认配置
        }
    }
}

最佳实践

使用抽象属性

kotlin
abstract class MyTask : DefaultTask() {
    @get:Input
    abstract val prop: Property<String>  // 推荐
}

声明输入输出

  • 支持增量构建
  • 支持缓存
  • 提升性能

使用 Provider API

  • 延迟计算
  • 配置缓存兼容
  • 避免配置阶段执行

添加 @CacheableTask

  • 支持构建缓存
  • 跨机器共享
  • 提升CI速度

使用 PathSensitive

  • 正确的路径敏感性
  • 避免不必要的重新执行

验证输入

kotlin
@TaskAction
fun execute() {
    require(inputFile.get().asFile.exists()) {
        "Input file does not exist"
    }
}

处理可选输入

kotlin
@get:Input
@get:Optional
abstract val optionalProp: Property<String>