3. 缓存键的构成与影响因素
Gradle 构建缓存的命中与否由 缓存键(Cache Key) 唯一决定。一个任务或工件的缓存键是其所有 输入(Inputs) 的散列摘要。当输入发生任何变化时,缓存键改变,旧缓存失效。本章深入剖析缓存键的构成要素,并重点分析输入文件、注解处理器、插件版本、环境变量四个核心影响因素,以及它们在实践中如何导致缓存未命中。
3.1 缓存键的组成原理
Gradle 使用 DefaultCacheKey 实现,其算法可简化为:
CacheKey = HASH(
任务类型完整类名 +
所有 @Input 注解属性的序列化值 +
所有 @InputFiles 注解文件的路径与内容哈希 +
所有 @InputDirectory 注解目录的目录快照 +
所有 @Nested 注解内部对象的递归哈希 +
插件版本标识 +
环境变量子集 +
全局缓存配置
)
其中 输入文件 包括源文件、资源文件、脚本文件、编译配置文件等。注解处理器 在 Java/Kotlin 编译任务中作为特殊输入参与。插件版本 决定了任务实现的字节码,变化会导致全量缓存失效。环境变量 则通过 @Input 或 @Option 显式声明后纳入键运算。
3.2 影响因素详解
| 影响因素 | 作用方式 | 典型引起缓存未命中的场景 |
|---|---|---|
| 输入文件 | 文件路径、内容哈希、目录结构 | 注释变更、空行增减、生成文件时间戳不同 |
| 注解处理器 | 处理器类路径、参数、项目相对路径 | 处理器版本升级、参数顺序变化 |
| 插件版本 | 插件坐标(group:name:version) | 从 8.0 升级到 8.1,所有任务缓存失效 |
| 环境变量 | 通过 @Option / systemProperty 显式声明 |
CI 节点使用不同 JAVA_HOME 路径 |
陷阱提醒: 隐式环境变量(如
PATH、HOME)默认不参与缓存键计算,但若任务内部依赖它们,会造成“不同环境相同键→执行结果不同”的隐患。应通过 @Input 显式声明。
3.3 影响流程示意
下图展示了构建缓存键从输入收集到哈希计算的完整流程,并标注了四个关键影响因素的介入点:
3.4 调试缓存键变化
使用 --info 或 --debug 日志可查看缓存键详情:
# 查看某个任务的缓存键
gradle :app:compileJava --info | grep "Cache key"
# 输出示例片段
[INFO] [org.gradle.api.internal.tasks.execution.CacheableTaskExecuter]
Cache key for task ':app:compileJava' is:
class name: org.gradle.api.tasks.compile.JavaCompile
inputs {
classpath: d41d8cd98f00b204e9800998ecf8427e
source: a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a
annotationProcessing: processors=[com.example.MyProcessor], version=1.0.0
pluginVersion: org.gradle.api.plugins.java:8.0
environment: JDK_HOME=/usr/lib/jvm/java-17-openjdk
}
3.5 常见错误与稳定性优化
- 错误1: 注解处理器未声明为
@CacheableTask的输入,导致处理器参数变化不被感知。解决方案:在自定义任务中显式声明@Input处理器配置。 - 错误2: 插件版本使用动态版本号(如
8.+),每次解析可能不同,缓存键意外变化。应锁定 exact version。 - 错误3: 环境变量如
CI或BUILD_NUMBER被隐式使用,造成不同构建节点结果相同但键不同。应使用@Input只声明必须的环境变量,排除噪音。
优化实践:
- 对不参与缓存计算的文件使用
@Ignore注解。 - 对目录输入使用
@PathSensitive(RELATIVE)而非ABSOLUTE,避免路径差异。 - 使用
org.gradle.caching.inputs.ignore(FileCollection)API 排除构建缓存中的大型生成文件。
3.6 配置示例:显式声明缓存键环境变量
// build.gradle.kts
abstract class MyTask : DefaultTask() {
@get:Input
abstract val jdkHome: Property<String>
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputFile: RegularFileProperty
@TaskAction
fun execute() {
// 任务逻辑
println("JDK: ${jdkHome.get()}, Input: ${inputFile.get()}")
}
}
tasks.register<MyTask>("checkEnvironment") {
jdkHome.set(System.getenv("JAVA_HOME") ?: "/usr/lib/jvm/default")
inputFile.set(layout.projectDirectory.file("src/main/resources/data.txt"))
}
调试技巧: 使用
gradle -p myproject build --scan 生成构建扫描,在“Build Cache”面板中查看每个任务的缓存键哈希以及输入详情,快速定位无效输入。
3.7 部署关注点
在持续集成环境中,不同节点(如临时容器、不同操作系统)可能产生不同的路径、用户目录、临时文件路径。应统一配置:
# gradle.properties (CI 统一)
org.gradle.caching=true
org.gradle.caching.debug=false
# 明确指定构建缓存键中要排除的环境变量(示例)
systemProp.org.gradle.caching.inputs.ignoreEnvVars=CI,USER,HOME
通过以上手段,可以将缓存命中率从无序的 30% 提升至稳定 80% 以上,大幅减少重复编译时间。