自定义 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
// app/build.gradle.kts
abstract class SimpleTask : DefaultTask() {
@TaskAction
fun execute() {
println("Simple task")
}
}
tasks.register<SimpleTask>("simple")方式二:buildSrc 目录
创建 buildSrc:
// buildSrc/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
google()
mavenCentral()
}定义任务类:
// 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")
}
}使用:
// app/build.gradle.kts
import tasks.MyCustomTask
tasks.register<MyCustomTask>("myTask")方式三:独立插件
插件项目:
// my-plugin/build.gradle.kts
plugins {
`kotlin-dsl`
`maven-publish`
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "com.example.my-plugin"
implementationClass = "com.example.MyPlugin"
}
}
}// my-plugin/src/main/kotlin/com/example/MyPlugin.kt
class MyPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.tasks.register<MyCustomTask>("myTask")
}
}使用:
// settings.gradle.kts
includeBuild("../my-plugin")// build.gradle.kts
plugins {
id("com.example.my-plugin")
}创建自定义任务
Ad-hoc 任务 简单场景
基本语法:
tasks.register("myTask") {
doLast {
println("执行任务")
}
}带属性:
tasks.register("greet") {
val name = "Gradle"
doLast {
println("Hello, $name!")
}
}任务类 推荐
定义任务类:
abstract class GreetTask : DefaultTask() {
@get:Input
abstract val greeting: Property<String>
@TaskAction
fun greet() {
println(greeting.get())
}
}注册任务:
tasks.register<GreetTask>("greet") {
greeting.set("Hello, Gradle!")
}任务属性
为什么需要声明输入输出
在 Gradle 中,任务的输入和输出必须显式声明,这样 Gradle 才能:
实现增量构建:
- 输入未改变 → 任务跳过(UP-TO-DATE)
- 仅输入改变 → 任务重新执行
- 节省大量构建时间
支持构建缓存:
- 相同输入 → 复用缓存结果
- 跨机器共享缓存
- CI 和本地共享构建产物
任务依赖追踪:
- 自动建立任务依赖关系
- A 任务的输出 = B 任务的输入 → 自动依赖
- 无需手动 dependsOn
@Input 输入属性
作用:标记简单值类型的输入(字符串、数字、布尔值等)
为什么需要:
- Gradle 需要知道哪些值会影响任务结果
- 值改变时重新执行任务
- 值不变时跳过任务
适用场景:
- 版本号
- 配置开关
- 简单参数
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()}")
}
}使用示例:
tasks.register<ProcessTask>("process") {
message.set("Hello, Gradle!")
prefix.set(">>> ") // 可选,可以不设置
}效果:
- 首次运行:执行任务
- message 不变:跳过任务(UP-TO-DATE)
- message 改变:重新执行
@InputFile 输入文件
作用:标记单个文件输入
为什么需要:
- Gradle 监控文件内容变化
- 文件内容改变 → 重新执行
- 文件未改变 → 跳过任务
适用场景:
- 配置文件
- 模板文件
- 数据文件
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 多个文件
作用:标记多个文件输入(文件集合)
为什么需要:
- 处理一批文件
- 任何文件改变都触发重新执行
适用场景:
- 源代码文件
- 资源文件
- 配置文件集合
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 输入目录
作用:标记整个目录作为输入
为什么需要:
- 监控目录下所有文件
- 目录内任何文件改变都触发执行
- 适合处理整个目录的场景
适用场景:
- 资源目录
- 源码目录
- 文档目录
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 管理输出文件
- 支持增量构建
- 支持缓存
适用场景:
- 生成报告文件
- 编译产物
- 转换后的文件
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 输出目录
作用:标记任务会生成整个目录
为什么需要:
- 管理多个输出文件
- 清理旧的输出
- 支持缓存
适用场景:
- 生成多个报告
- 编译输出目录
- 批量处理结果
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 的检查流程:
- 检查输入:计算所有 @Input/@InputFile 的值/校验和
- 检查输出:检查 @OutputFile/@OutputDirectory 是否存在
- 决策:
- 输入未改变 + 输出存在 → 跳过(UP-TO-DATE)
- 输入改变 或 输出不存在 → 执行
基本增量任务
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"))
}运行效果:
# 首次运行
$ ./gradlew transform
> Task :transform
BUILD SUCCESSFUL
# 再次运行(输入未改变)
$ ./gradlew transform
> Task :transform UP-TO-DATE
BUILD SUCCESSFUL
# 修改 input.txt 后运行
$ ./gradlew transform
> Task :transform
BUILD SUCCESSFUL@SkipWhenEmpty
作用:当输入文件为空时跳过任务
为什么需要:
- 避免无意义的执行
- 没有输入就没有输出
适用场景:
- 源文件处理
- 资源编译
- 可选的任务
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 | 忽略路径,仅内容 | 资源文件 |
abstract class CompileTask : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE) // 相对路径敏感
abstract val sources: ConfigurableFileCollection
@TaskAction
fun compile() {
// Java 编译依赖包名,需要相对路径
sources.forEach { println("Compiling: $it") }
}
}示例场景:
// 场景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
作用:标记任务可以使用构建缓存
@CacheableTask
abstract class ProcessDataTask : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val inputData: RegularFileProperty
@get:OutputFile
abstract val result: RegularFileProperty
@TaskAction
fun process() {
// 处理数据
}
}启用缓存:
# gradle.properties
org.gradle.caching=trueProvider API 集成
使用 Provider
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())
}延迟计算
val gitHash = providers.exec {
commandLine("git", "rev-parse", "HEAD")
}.standardOutput.asText.map { it.trim() }
tasks.register<GenerateTask>("generate") {
hash.set(gitHash)
}任务验证
@Internal 内部属性
abstract class BuildTask : DefaultTask() {
@get:Internal
abstract val tempDir: DirectoryProperty
@get:InputFile
abstract val source: RegularFileProperty
@get:OutputFile
abstract val output: RegularFileProperty
}@Console 控制台属性
abstract class InteractiveTask : DefaultTask() {
@get:Console
abstract val verbose: Property<Boolean>
@TaskAction
fun execute() {
if (verbose.get()) {
println("Verbose output...")
}
}
}实战案例
案例1:代码生成任务
@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:资源打包任务
@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:版本信息任务
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 中定义
// 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() {
// 任务逻辑
}
}使用:
// app/build.gradle.kts
tasks.register<MyCustomTask>("myTask") {
input.set(file("input.txt"))
output.set(file("output.txt"))
}约定插件
// buildSrc/src/main/kotlin/MyPlugin.kt
class MyPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.tasks.register<MyCustomTask>("myTask") {
// 默认配置
}
}
}最佳实践
使用抽象属性:
abstract class MyTask : DefaultTask() {
@get:Input
abstract val prop: Property<String> // 推荐
}声明输入输出:
- 支持增量构建
- 支持缓存
- 提升性能
使用 Provider API:
- 延迟计算
- 配置缓存兼容
- 避免配置阶段执行
添加 @CacheableTask:
- 支持构建缓存
- 跨机器共享
- 提升CI速度
使用 PathSensitive:
- 正确的路径敏感性
- 避免不必要的重新执行
验证输入:
@TaskAction
fun execute() {
require(inputFile.get().asFile.exists()) {
"Input file does not exist"
}
}处理可选输入:
@get:Input
@get:Optional
abstract val optionalProp: Property<String>