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 影响流程示意

下图展示了构建缓存键从输入收集到哈希计算的完整流程,并标注了四个关键影响因素的介入点:

缓存键构成与影响因素流程图 缓存键构成与影响因素 Gradle 任务执行 收集所有 @Input / @InputFiles 等输入 注解处理器、插件版本、环境变量均在此时纳入 输入文件 路径·内容哈希 目录结构快照 注解处理器 类路径·参数 版本与顺序 插件版本 坐标标识 实现字节码 环境变量 @Option 声明 系统属性 归一化 + 哈希计算 SHA-256 最终缓存键 注意:任何输入变化都会导致最终缓存键改变,从而触发缓存未命中。 对不可重现的输入(如时间戳)应使用 @Ignore 或 @PathSensitive(RELATIVE) 优化。

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% 以上,大幅减少重复编译时间。