主题
第 5 章 Task 全攻略:从 Hello 到企业级自定义 Task
学习目标:会写各种 Task —— 简单的、Typed 的、自定义类的;能让自定义 Task 支持增量构建(这是 Gradle 性能的核心);理解 inputs / outputs / @TaskAction 三大注解;知道何时该用 doLast,何时该写专门的
Task类。
5.1 Task 是什么的回顾
第 3 章已经讲过 Task 的基本概念,本章深入讲"怎么写好一个 Task"。
每个 Task 的"完整画像":
┌──────────────────────────────────────────────────────────┐
│ Task │
├──────────────────────────────────────────────────────────┤
│ ① name — 在 Project 内唯一 │
│ ② group + description — 在 ./gradlew tasks 列表里显示 │
│ ③ dependsOn / mustRunAfter / finalizedBy — 跟其他 Task 关系 │
│ ④ inputs — 这个 Task 看哪些数据 │
│ ⑤ outputs — 这个 Task 产出什么 │
│ ⑥ actions (doFirst/doLast/@TaskAction) — 真正干活的代码 │
│ ⑦ enabled / onlyIf — 控制是否执行 │
└──────────────────────────────────────────────────────────┘
★ ④⑤ 是增量构建 / 构建缓存的基石:声明清楚了才会被跳过5.2 三种"写 Task"的方式
按从简到繁排:
5.2.1 方式 1:临时 Task(doLast 块)
适合快速一次性脚本:
kotlin
tasks.register("printVersion") {
doLast {
println("当前版本:${project.version}")
}
}缺点:没有 input/output,每次都全量执行。不能复用。
5.2.2 方式 2:Typed Task(用 Gradle 内置类型)
适合用现成的 Task 类型组装:
kotlin
tasks.register<Copy>("copyConfig") {
from("src/main/resources/config")
into(layout.buildDirectory.dir("config"))
include("**/*.yml")
}
tasks.register<Zip>("releaseZip") {
archiveFileName = "release-${project.version}.zip"
destinationDirectory = layout.buildDirectory.dir("dist")
from(layout.buildDirectory.dir("classes"))
}
tasks.register<Exec>("dockerBuild") {
commandLine("docker", "build", "-t", "myapp:${project.version}", ".")
}优点:内置类型已经声明了 input/output,自动支持增量构建。
5.2.3 方式 3:自定义 Task 类
适合复杂、要复用、要有完整生命周期的 Task:
kotlin
// 一个统计代码行数的 Task
abstract class CountLinesTask : DefaultTask() {
@get:InputDirectory
abstract val sourceDir: DirectoryProperty
@get:Input
abstract val fileExtension: Property<String>
@get:OutputFile
abstract val report: RegularFileProperty
@TaskAction
fun count() {
val pattern = "**/*.${fileExtension.get()}"
val files = sourceDir.get().asFileTree.matching { include(pattern) }.files
val total = files.sumOf { it.readLines().size }
report.get().asFile.writeText(
"Files: ${files.size}\nTotal lines: $total\n"
)
println("✅ Counted $total lines in ${files.size} files → ${report.get().asFile}")
}
}
// 注册并配置
tasks.register<CountLinesTask>("countJavaLines") {
sourceDir = layout.projectDirectory.dir("src/main/java")
fileExtension = "java"
report = layout.buildDirectory.file("reports/lines.txt")
}最重要:这种写法 自动支持增量构建 —— sourceDir 没变就跳过,跑出 UP-TO-DATE。
5.3 增量构建:Gradle 性能的核心
5.3.1 什么是增量构建?
第 1 章讲过:没改的就别重跑。Gradle 用 inputs / outputs 的 hash 指纹决定是否跳过。
第一次跑 myTask:
input hash: A1B2C3
output hash: X9Y8Z7
→ 实际执行 → 写入 .gradle/ 缓存
第二次跑 myTask:
input hash: A1B2C3 ← 没变
output hash: X9Y8Z7 ← 也没变
→ 命中缓存 → UP-TO-DATE → 跳过!
第三次跑(你改了源码):
input hash: A1B2C3 ← 变成 D4E5F6
→ 跟缓存对不上 → 重新执行5.3.2 怎么声明 inputs / outputs
方式 A:临时 Task 用 inputs/outputs API:
kotlin
tasks.register("processData") {
inputs.file("data/input.csv")
inputs.dir("data/templates")
inputs.property("environment", project.property("env"))
outputs.file(layout.buildDirectory.file("output.json"))
outputs.dir(layout.buildDirectory.dir("processed"))
doLast {
// 真正干活
}
}方式 B:自定义 Task 类用注解(强烈推荐):
kotlin
abstract class ProcessDataTask : DefaultTask() {
@get:InputFile
abstract val inputCsv: RegularFileProperty
@get:InputDirectory
abstract val templatesDir: DirectoryProperty
@get:Input
abstract val environment: Property<String>
@get:OutputFile
abstract val outputJson: RegularFileProperty
@TaskAction
fun process() {
// 真正干活
}
}5.3.3 输入输出注解大全
| 注解 | 作用 | 类型 |
|---|---|---|
@Input | 标量输入(字符串、数字、boolean) | Property<T> |
@InputFile | 单个文件输入 | RegularFileProperty |
@InputDirectory | 目录输入(递归) | DirectoryProperty |
@InputFiles | 多个文件输入 | ConfigurableFileCollection |
@OutputFile | 单个文件输出 | RegularFileProperty |
@OutputDirectory | 目录输出 | DirectoryProperty |
@OutputFiles | 多个文件输出 | ConfigurableFileCollection |
@Optional | 标记为可选(不强制要求) | / |
@Incremental | 增量输入(只处理变化的文件) | InputChanges |
@CacheableTask | 标记此 Task 支持构建缓存 | (类级别注解) |
5.3.4 真实场景:把任务从"全量"改造成"增量"
改造前(每次都重跑):
kotlin
tasks.register("renderTemplates") {
doLast {
val templates = file("templates").listFiles().orEmpty()
templates.forEach { tpl ->
val rendered = tpl.readText().replace("{{name}}", "World")
file("build/rendered/${tpl.name}").also {
it.parentFile.mkdirs()
}.writeText(rendered)
}
}
}
// 跑两次:
// 第一次:执行(5s)
// 第二次:还是执行(5s) ← 浪费!改造后(增量友好):
kotlin
tasks.register("renderTemplates") {
inputs.dir("templates")
inputs.property("name", "World")
outputs.dir(layout.buildDirectory.dir("rendered"))
doLast {
val templates = file("templates").listFiles().orEmpty()
templates.forEach { tpl ->
val rendered = tpl.readText().replace("{{name}}", "World")
layout.buildDirectory.file("rendered/${tpl.name}").get().asFile
.also { it.parentFile.mkdirs() }
.writeText(rendered)
}
}
}
// 跑两次:
// 第一次:执行(5s)
// 第二次:UP-TO-DATE ← 0.1s 完成!📌 生活化类比:增量构建像"周末妈妈做家务"。第一次大扫除(input = 房间状态)→ 写满 100 个清单(output)。下周再来:发现房间没变(input hash 一致),就不用重做了。
5.4 enabled vs onlyIf:控制 Task 是否执行
5.4.1 enabled = false(直接禁用)
kotlin
tasks.register("foo") {
enabled = false
doLast { println("永远不会跑") }
}跑 ./gradlew foo:
> Task :foo SKIPPED5.4.2 onlyIf {}(条件执行)
kotlin
tasks.register("uploadToS3") {
onlyIf {
project.hasProperty("release")
}
doLast { println("Uploading...") }
}跑:
bash
$ ./gradlew uploadToS3
> Task :uploadToS3 SKIPPED ← 条件不满足,跳过
$ ./gradlew uploadToS3 -Prelease
> Task :uploadToS3
Uploading... ← 条件满足,执行💡 实战:
onlyIf比enabled更灵活,能在配置阶段读环境变量、project property 等。
5.5 几种典型自定义 Task 完整示例
5.5.1 例 1:发版前自动更新版本号
kotlin
abstract class BumpVersionTask : DefaultTask() {
@get:InputFile
abstract val versionFile: RegularFileProperty
@get:Input
@get:Optional
abstract val newVersion: Property<String>
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun bump() {
val current = versionFile.get().asFile.readText().trim()
val next = newVersion.orNull ?: nextPatch(current)
outputFile.get().asFile.writeText(next)
println("📦 Version: $current → $next")
}
private fun nextPatch(v: String): String {
val parts = v.split(".").map { it.toInt() }.toMutableList()
parts[2] = parts[2] + 1
return parts.joinToString(".")
}
}
tasks.register<BumpVersionTask>("bumpVersion") {
versionFile = layout.projectDirectory.file("VERSION")
outputFile = layout.projectDirectory.file("VERSION")
newVersion = providers.gradleProperty("v").orNull
}
// 跑:./gradlew bumpVersion → 自动 patch+1
// 跑:./gradlew bumpVersion -Pv=2.0.0 → 强制改成 2.0.05.5.2 例 2:编译前自动生成 BuildInfo 类
kotlin
abstract class GenerateBuildInfoTask : DefaultTask() {
@get:Input
abstract val packageName: Property<String>
@get:Input
abstract val version: Property<String>
@get:Input
abstract val buildTime: Property<String>
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
val pkg = packageName.get()
val pkgPath = pkg.replace('.', '/')
val target = outputDir.get().asFile.resolve("$pkgPath/BuildInfo.java")
target.parentFile.mkdirs()
target.writeText("""
package $pkg;
public final class BuildInfo {
public static final String VERSION = "${version.get()}";
public static final String BUILD_TIME = "${buildTime.get()}";
private BuildInfo() {}
}
""".trimIndent())
println("📝 Generated: ${target.absolutePath}")
}
}
tasks.register<GenerateBuildInfoTask>("generateBuildInfo") {
packageName = "com.example.generated"
version = project.version.toString()
buildTime = java.time.LocalDateTime.now().toString()
outputDir = layout.buildDirectory.dir("generated/sources/buildInfo/java")
}
// 把生成目录加到源码集
sourceSets["main"].java.srcDir(
tasks.named<GenerateBuildInfoTask>("generateBuildInfo").map { it.outputDir }
)
// 让 compileJava 依赖此 task
tasks.named("compileJava") {
dependsOn("generateBuildInfo")
}跑 ./gradlew compileJava 时会自动先跑 generateBuildInfo,生成 BuildInfo.java,然后参与编译。典型代码生成场景。
5.5.3 例 3:调外部命令的 Task
kotlin
abstract class GitInfoTask : DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty
@get:Inject
abstract val execOperations: ExecOperations
@TaskAction
fun run() {
val out = java.io.ByteArrayOutputStream()
execOperations.exec {
commandLine("git", "rev-parse", "--short", "HEAD")
standardOutput = out
}
val sha = out.toString().trim()
outputFile.get().asFile.writeText("git.sha=$sha\n")
println("🌿 Git SHA: $sha")
}
}
tasks.register<GitInfoTask>("captureGitInfo") {
outputFile = layout.buildDirectory.file("git.properties")
}5.5.4 例 4:增量处理大量文件
kotlin
abstract class TransformFilesTask : DefaultTask() {
@get:InputDirectory
@get:Incremental
abstract val sourceDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun execute(inputChanges: InputChanges) {
if (!inputChanges.isIncremental) {
// 全量执行(首次或缓存失效)
outputDir.get().asFile.deleteRecursively()
outputDir.get().asFile.mkdirs()
}
inputChanges.getFileChanges(sourceDir).forEach { change ->
val target = outputDir.get().file(change.normalizedPath).asFile
when (change.changeType) {
ChangeType.ADDED, ChangeType.MODIFIED -> {
target.parentFile.mkdirs()
target.writeText(change.file.readText().uppercase())
println("📝 Transformed: ${change.normalizedPath}")
}
ChangeType.REMOVED -> {
target.delete()
println("❌ Deleted: ${change.normalizedPath}")
}
}
}
}
}@Incremental + InputChanges 让 Task 只处理"变化的那部分文件",对处理千百个文件的 Task(如代码生成、资源处理)性能提升巨大。
5.6 Task 跨阶段交互:Provider / Property API
5.6.1 痛点:配置阶段拿不到执行阶段的值
kotlin
// ❌ 错的:configure 阶段 myTask 还没跑,outputFile 不存在
val taskA = tasks.register("myTask") {
outputs.file("foo.txt")
doLast { file("foo.txt").writeText("hello") }
}
val taskB = tasks.register("dependent") {
val content = file("foo.txt").readText() // ← 配置阶段就读,foo.txt 还没生成!
doLast { println(content) }
}5.6.2 解法:用 Provider 延迟取值
kotlin
// ✅ 正确:把"取值"包装成 Provider,执行时才求值
val taskA = tasks.register("myTask") {
val output = layout.buildDirectory.file("foo.txt")
outputs.file(output)
doLast { output.get().asFile.writeText("hello") }
}
val taskB = tasks.register("dependent") {
dependsOn(taskA)
doLast {
val content = taskA.get().outputs.files.singleFile.readText()
println(content)
}
}5.6.3 优雅写法:用 Provider chaining
kotlin
val versionFile = layout.buildDirectory.file("version.txt")
val createVersion = tasks.register("createVersion") {
outputs.file(versionFile)
doLast {
versionFile.get().asFile.writeText("1.2.3")
}
}
val packageApp = tasks.register("packageApp") {
inputs.file(versionFile)
dependsOn(createVersion)
doLast {
val v = versionFile.get().asFile.readText().trim()
println("📦 Packaging app version $v")
}
}📌 这里
versionFile是Provider<RegularFile>,是懒求值的:直到doLast里.get()时才真正读文件路径。这种模式 Configuration Cache 友好。
5.7 章末小结
★ 第 5 章核心知识图谱 ★
│
┌─────────────────────┼─────────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼─────┐
│ 三种写法 │ │ 增量构建 │ │ 进阶 API │
├─────────┤ ├──────────┤ ├──────────┤
│ doLast │ │ inputs │ │ Provider │
│ Typed │ │ outputs │ │ Property │
│ 自定义类 │ │ @Input │ │ Optional │
│ │ │ @Output │ │ onlyIf │
└─────────┘ └──────────┘ └──────────┘
↑
★ 性能差距 5-10 倍 ★🎤 5.8 章末面试题(10 道高频题)
Q1. 同样是注册 Task,为什么用 tasks.register 比 tasks.create 好?
答:register 是懒注册:只把"配置闭包"存起来,直到 Task 真的被需要时才执行配置。create 是立即创建:配置阶段一遇到就跑闭包。
好处:
- 没用到的 Task 不耗时;
- 配置缓存友好;
- Gradle 5+ 推荐做法。
工程上默认全部用 register,不要用 create。
Q2. 如何让一个自定义 Task 支持增量构建?
答:3 步:
- 声明输入:用
@Input/@InputFile/@InputDirectory/@InputFiles注解或inputs.file(...)API。 - 声明输出:用
@OutputFile/@OutputDirectory注解或outputs.file(...)API。 - 真正的动作放在
@TaskAction标记的方法里(自定义类)或doLast {}里(临时 task)。
声明完整了,Gradle 会自动比对 input/output 的 hash 决定是否跳过。
Q3. doFirst / doLast 和 @TaskAction 的关系?
答:
@TaskAction:自定义 Task 类主要执行体的标识,方法被调用时执行 Task 的核心逻辑。doFirst { }:在 @TaskAction 之前插入额外动作(多个 doFirst 倒序执行)。doLast { }:在 @TaskAction 之后追加额外动作。
执行顺序:doFirst-N → ... → doFirst-1 → @TaskAction → doLast-1 → ... → doLast-N。
💡 99% 时候用
doLast就够了,@TaskAction是更"工程化"的写法。
Q4. @Input 和 @InputFile 有什么区别?
答:
@Input:标量输入(字符串、数字、布尔、枚举),值本身参与 hash 计算。@InputFile:文件输入,文件内容的 hash 参与计算(不是路径!改名不算变)。
类似的:
@OutputFile是文件,路径要明确;@OutputDirectory是目录,目录下所有文件都视为输出。
坑:
- 如果一个属性只是路径不是要监听的文件内容,应该用
@PathSensitive(PathSensitivity.NONE)+@InputFile,否则换路径就触发重跑。
Q5. 我有一个 Task 不希望它每次都跑,怎么控制?
答:3 种手段:
- 声明 input/output(最佳)→ 没变就 UP-TO-DATE 自动跳过;
onlyIf {}→ 加条件判断,不满足就 SKIPPED;enabled = false→ 临时禁用,所有时候都不跑。
| 手段 | 何时用 |
|---|---|
| input/output | 通用,最理想 |
| onlyIf | 依赖动态条件(系统变量、project property) |
| enabled | 临时调试 / 永久禁用 |
Q6. 自定义 Task 类为啥要用 abstract class 而不是 open class?
答:Gradle 会自动生成抽象属性的实现(运行时通过字节码增强)。所以你只需要声明:
kotlin
abstract class MyTask : DefaultTask() {
@get:Input
abstract val input: Property<String> // 抽象,Gradle 帮你实现
@TaskAction
fun run() {
println(input.get())
}
}Gradle 会自动给 input 注入一个 Property<String> 实例。不写 abstract 反而要自己手动实现 Property,更麻烦。
Q7. 一个 Task 怎么消费另一个 Task 的输出?
答:用 Provider(懒求值)传递:
kotlin
val genTask = tasks.register<GenerateTask>("gen") {
output = layout.buildDirectory.file("gen.txt")
}
val useTask = tasks.register("use") {
inputs.file(genTask.flatMap { it.output }) // ← 用 flatMap 链 Provider
doLast { /* ... */ }
}好处:
- 自动声明 task 依赖(Gradle 看到 inputs 来自 genTask 的 output,自动加 dependsOn);
- 懒求值,配置阶段不实际计算;
- 配置缓存友好。
反模式:直接 genTask.get().output.get() 在配置阶段取值 —— 会 eager 求值,破坏 lazy 优势。
Q8. @CacheableTask 注解什么时候加?
答:@CacheableTask 标记一个 Task 的输出可以被构建缓存(Build Cache)复用。
- 不加:只能"本地增量"(input/output 没变就跳过)
- 加了:input 一致时能从远程 / 本地缓存取出 output,不用重跑
适合加的场景:
- 输入完全决定输出(纯函数式)
- 输出是文件/目录(不是控制台输出之类的副作用)
- 输入声明完整(包括 environment、JVM 版本等隐含因素)
典型例子:compileJava、Test、Jar 这些内置 Task 都是 @CacheableTask。
Q9. 怎么写一个"增量处理输入文件"的 Task?
答:用 @Incremental + InputChanges:
kotlin
abstract class TransformFilesTask : DefaultTask() {
@get:InputDirectory
@get:Incremental
abstract val src: DirectoryProperty
@get:OutputDirectory
abstract val dst: DirectoryProperty
@TaskAction
fun run(changes: InputChanges) {
if (!changes.isIncremental) {
dst.get().asFile.deleteRecursively()
dst.get().asFile.mkdirs()
}
changes.getFileChanges(src).forEach { c ->
// 只处理变化的文件
when (c.changeType) {
ChangeType.ADDED, ChangeType.MODIFIED -> { /* ... */ }
ChangeType.REMOVED -> { /* 删 */ }
}
}
}
}好处:源代码 1000 个文件,改 1 个 → 只处理 1 个,性能提升 1000 倍。常见于代码生成、资源处理、文档转换等场景。
Q10. tasks.named 和 tasks.findByName 有什么区别?
答:
tasks.named("foo"):懒求值,返回TaskProvider<Task>。如果 task 不存在会抛 UnknownTaskException,但不会立即配置该 task。tasks.findByName("foo"):立即查找,返回Task?(可能为 null)。会立即触发该 task 的配置。
推荐:99% 情况下用 tasks.named,因为:
- 懒,更快;
- 配置缓存友好;
- 失败时报错更清晰。
findByName 只在"判断 task 是否存在 + 不需要配置它"的场景用。
下一章 → 第 6 章 · 依赖管理 →
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
txt
/*
* 第 5 章 · Task 全攻略 — 完整可跑示例
*
* 包含:
* - 三种 Task 写法(doLast / Typed / 自定义类)
* - 增量构建(input/output 声明)
* - 增量处理输入文件(@Incremental + InputChanges)
* - Provider 链式
* - onlyIf / enabled 控制执行
*
* 跑:
* ./gradlew tasks --group ch05-demo
* ./gradlew renderTemplates —— 看增量构建效果(再跑一次会 UP-TO-DATE)
* ./gradlew countLines —— 自定义 Task 类示例
* ./gradlew generateBuildInfo —— 生成 BuildInfo.java
* ./gradlew releaseZip —— 综合:依赖链 + Typed Task
*/
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import org.gradle.work.ChangeType
plugins {
base
}
version = "1.0.0"
// ===========================================================================
// 写法 1:临时 Task(doLast)
// 没声明 input/output → 每次都跑(演示反面教材)
// ===========================================================================
tasks.register("badTask") {
group = "ch05-demo"
description = "反面教材:每次都全量执行的 Task"
doLast {
println("⏰ Bad task: 我每次都被执行(即使没改任何东西)")
Thread.sleep(800)
}
}
// ===========================================================================
// 写法 1+:临时 Task + input/output 声明 → 增量构建生效
// ===========================================================================
tasks.register("renderTemplates") {
group = "ch05-demo"
description = "增量构建演示:渲染 templates/ 下的所有文件"
val srcDir = layout.projectDirectory.dir("templates")
val outDir = layout.buildDirectory.dir("rendered")
inputs.dir(srcDir).optional()
inputs.property("name", "World")
outputs.dir(outDir)
doLast {
if (!srcDir.asFile.exists()) {
srcDir.asFile.mkdirs()
srcDir.asFile.resolve("hello.txt").writeText("Hello, {{name}}!")
}
outDir.get().asFile.mkdirs()
srcDir.asFile.listFiles().orEmpty().forEach { f ->
val rendered = f.readText().replace("{{name}}", "World")
outDir.get().file(f.name).asFile.writeText(rendered)
}
println("✅ Rendered " + srcDir.asFile.listFiles().orEmpty().size + " templates")
}
}
// ===========================================================================
// 写法 2:Typed Task —— 用内置类型
// ===========================================================================
tasks.register<Copy>("copyDocs") {
group = "ch05-demo"
description = "演示 Copy 类型"
from("docs") {
include("**/*.md")
}
into(layout.buildDirectory.dir("docs"))
}
tasks.register<Zip>("releaseZip") {
group = "ch05-demo"
description = "综合演示:聚合多个产物为 zip"
dependsOn("copyDocs", "renderTemplates")
archiveFileName = "release-" + project.version + ".zip"
destinationDirectory = layout.buildDirectory.dir("releases")
from(layout.buildDirectory.dir("docs")) { into("docs") }
from(layout.buildDirectory.dir("rendered")) { into("rendered") }
}
// ===========================================================================
// 写法 3:自定义 Task 类(推荐)
// ===========================================================================
@CacheableTask
abstract class CountLinesTask : DefaultTask() {
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val sourceDir: DirectoryProperty
@get:Input
abstract val fileExtension: Property<String>
@get:OutputFile
abstract val report: RegularFileProperty
@TaskAction
fun count() {
val pattern = "**/*." + fileExtension.get()
val files = sourceDir.get().asFileTree.matching { include(pattern) }.files
val total = files.sumOf { it.readLines().size }
report.get().asFile.parentFile.mkdirs()
report.get().asFile.writeText("Files: " + files.size + "\nTotal lines: " + total + "\n")
println("📊 Counted " + total + " lines in " + files.size + " files → " + report.get().asFile)
}
}
tasks.register<CountLinesTask>("countLines") {
group = "ch05-demo"
description = "统计 Kotlin 源码行数"
sourceDir = layout.projectDirectory
fileExtension = "kts"
report = layout.buildDirectory.file("reports/lines.txt")
}
// ===========================================================================
// 自定义 Task:生成 BuildInfo.java(典型代码生成场景)
// ===========================================================================
abstract class GenerateBuildInfoTask : DefaultTask() {
@get:Input
abstract val packageName: Property<String>
@get:Input
abstract val version: Property<String>
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
val pkg = packageName.get()
val pkgPath = pkg.replace('.', '/')
val target = outputDir.get().asFile.resolve(pkgPath + "/BuildInfo.java")
target.parentFile.mkdirs()
target.writeText("""
package $pkg;
public final class BuildInfo {
public static final String VERSION = "${'$'}{version.get()}";
public static final String BUILD_TIME = "${'$'}{System.currentTimeMillis()}";
private BuildInfo() {}
}
""".trimIndent())
println("📝 Generated: " + target.absolutePath)
}
}
tasks.register<GenerateBuildInfoTask>("generateBuildInfo") {
group = "ch05-demo"
description = "生成 BuildInfo.java 类(演示代码生成)"
packageName = "com.example.generated"
version = project.version.toString()
outputDir = layout.buildDirectory.dir("generated/sources/buildInfo/java")
}
// ===========================================================================
// 增量处理输入文件:@Incremental + InputChanges
// ===========================================================================
abstract class TransformFilesTask : DefaultTask() {
@get:InputDirectory
@get:Incremental
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val src: DirectoryProperty
@get:OutputDirectory
abstract val dst: DirectoryProperty
@TaskAction
fun execute(changes: InputChanges) {
if (!changes.isIncremental) {
println("🔄 Full rebuild")
dst.get().asFile.deleteRecursively()
dst.get().asFile.mkdirs()
} else {
println("⚡ Incremental rebuild")
}
changes.getFileChanges(src).forEach { c ->
val target = dst.get().file(c.normalizedPath).asFile
when (c.changeType) {
ChangeType.ADDED, ChangeType.MODIFIED -> {
target.parentFile.mkdirs()
target.writeText(c.file.readText().uppercase())
println(" 📝 " + c.changeType + ": " + c.normalizedPath)
}
ChangeType.REMOVED -> {
target.delete()
println(" ❌ REMOVED: " + c.normalizedPath)
}
}
}
}
}
tasks.register<TransformFilesTask>("transformFiles") {
group = "ch05-demo"
description = "增量处理:只对变化的文件做转换"
src = layout.projectDirectory.dir("data")
dst = layout.buildDirectory.dir("transformed")
}
// ===========================================================================
// 演示 onlyIf
// ===========================================================================
tasks.register("uploadToS3") {
group = "ch05-demo"
description = "只在 -Prelease 时执行"
onlyIf { project.hasProperty("release") }
doLast { println("📤 Uploading to S3...") }
}
// ===========================================================================
// 演示 finalizedBy
// ===========================================================================
tasks.register("riskyOperation") {
group = "ch05-demo"
description = "演示 finalizedBy:A 跑完后无论成败都跑 cleanup"
finalizedBy("cleanup")
doLast { println("⚠️ Risky operation running...") }
}
tasks.register("cleanup") {
group = "ch05-demo"
doLast { println("🧹 Cleanup running (always after riskyOperation)") }
}txt
rootProject.name = "ch05-task-demo"