自定义 Gradle Task 必须显式声明 inputs/outputs 才能启用构建缓存,否则会被跳过;需用 @CacheableTask 标注类、避免可变集合与隐式输入,并通过 build scan 验证缓存状态。

自定义 Gradle Task 必须显式声明 inputs/outputs
Gradle 构建缓存不会自动识别你写的 Task 是否可缓存——它只对“输入确定、输出明确、无副作用”的任务启用缓存。如果你写了一个自定义 task(比如打包配置文件、生成代码),默认情况下它会被跳过缓存,每次构建都重新执行。
必须手动用 inputs 和 outputs 告诉 Gradle:“这些是影响结果的输入”“这是本次任务真正产出的文件”。否则 Gradle 会认为该 task 不可缓存,日志里会出现 Skipping task ':myTask' as it has no outputs 或类似提示。
-
inputs.files("src/main/config")—— 声明目录为输入,内容变更即触发重执行 -
inputs.property("version", project.version)—— 声明字符串属性为输入,避免因版本号变化却未感知 -
outputs.dir(layout.buildDirectory.dir("generated"))—— 用layout.buildDirectory而不是硬拼"$buildDir/generated",否则配置缓存会失败 - 不要在 task 动作里调用
new Date()、System.currentTimeMillis()或读取未声明的环境变量,这会让缓存键不稳定
使用 Kotlin DSL 定义 task 时避免 mutable 集合
在 build.gradle.kts 中用 mutableListOf 或 mutableMapOf 收集 task 配置,会导致配置缓存序列化失败——因为 mutable 集合对象无法被安全地冻结和还原。Gradle 会报错类似 Cannot serialize object of type java.util.ArrayList。
正确做法是全部改用不可变容器,并在注册 task 时一次性声明清楚:
tasks.register<MyCodegenTask>("generateApi") {
inputs.property("apiSpec", layout.projectDirectory.file("openapi.yaml"))
outputs.dir(layout.buildDirectory.dir("generated/src"))
// ✅ 不要在这里做 list.add(...) 或 map.put(...)
}
如果逻辑复杂,把参数组装提前到 task 注册前,用 val 绑定好再传入,而不是在 task 执行时动态构造。
继承 DefaultTask 时需标注 @CacheableTask
当你写一个自定义 task 类(比如 class ZipAssetsTask : DefaultTask()),即使已声明 inputs/outputs,Gradle 默认仍不认为它可缓存。必须加上 @CacheableTask 注解,否则构建缓存完全忽略这个类。
注意两点:
- 注解必须加在 class 上,不是方法上;Kotlin 中要加
@get:CacheableTask(因注解目标是 getter) - 类中所有非 transient 字段,只要参与逻辑,都应通过
inputs/outputs显式暴露,否则可能造成缓存误命中 - 避免在 task 类构造函数里做 I/O 或状态初始化,构造阶段不参与缓存判定,但会影响配置阶段稳定性
插件 apply 后注册的 task 容易漏掉缓存声明
很多第三方插件(比如 com.github.johnrengelman.shadow)或自研插件会在 apply 时动态注册 task。这类 task 很容易没设置 inputs/outputs,尤其当它基于 Copy 或 Exec 等内置 task 扩展而来时。
验证方式很简单:执行 ./gradlew build --dry-run,看目标 task 是否标有 FROM-CACHE;或者运行 ./gradlew build --scan,在 Build Scan 的 Task Execution 页面里查缓存状态。
修复建议:
- 在插件
apply后立即对关键 task 补充声明:tasks.named("shadowJar") { outputs.file(layout.buildDirectory.file("libs/shadow.jar")) } - 优先用
tasks.withType<ShadowJar>而不是tasks.getByName,更安全且兼容配置缓存 - 如果插件本身不维护缓存兼容性(如某些老版本 kotlin-serialization 插件),考虑升级或换用官方推荐替代方案
最常被忽略的是 task 的“隐式输入”:比如依赖了某个 project.property 却没用 inputs.property 声明,缓存就会在 property 变更后继续复用旧结果——这种 bug 很难排查,只能靠严格声明兜底。

















