常见环境问题:JDK/Gradle版本不兼容导致快照差异

遇到过这么个事儿:本地写好的Compose测试,快照跑得漂漂亮亮,一提交到CI,直接就红了。我当时第一反应是「代码写错了?」可仔细一查,代码完全一样。后来发现——CI用的JDK版本跟我不一样。嗯,快照对比是在比二进制,JDK的微小差异就能让序列化结果、字节码结构发生变化,最终导致测试失败。

说白了,版本不兼容不是Compose的锅,而是构建环境的不一致。这节课我们就来拆一拆这个坑。

问题现象:一模一样的代码,两个结果

最常见的场景是这样的:

  • 本地用JDK 11 + Gradle 7.4,快照全部通过
  • CI上用JDK 17 + Gradle 8.0,同样的测试,部分快照Assert失败
  • 对比两个环境的快照文件,二进制完全相同?不一定,有时是布局偏移几个像素,有时是文本渲染顺序不同

为什么会这样?

Compose快照在截取时依赖系统的Graphics2D渲染管线,而JDK不同版本对字体、抗锯齿、图形变换的实现细节有差异。另外,Gradle的构建缓存策略不一样,可能导致Compile task的输入输出顺序改变,从而影响Compose Compiler输出的字节码顺序。

根源分析:JDK版本差异

我整理过几个核心差异点:

差异维度影响范围典型版本
字体指标TextLayout测量JDK 8→11(迁移到OpenType)
图像渲染BufferedImage类型JDK 11→17(新增ColorSpace支持)
序列化Bitmap序列化结构JDK 9+模块化后包路径变化
字符串编码文件路径/资源名称JDK 17+默认使用UTF-8

我记得之前有个项目,JDK 11本地快照显示的「确认」按钮,字体是宋体;CI上JDK 17渲染出来变成了微软雅黑——宽了几个像素,整个布局就崩了。所以,快照测试必须跑在同一个JDK上。

根源分析:Gradle版本差异

Gradle版本的影响更隐蔽,因为它不直接参与渲染,但能改变编译顺序和缓存命中:

  • Gradle 7.x 和 8.x 的Task输入规范化规则不同
  • 增量编译的类文件顺序可能改变
  • Compose Compiler Plugin内部使用Gradle API获取目录结构,API变动会导致输出文件hash不一样

一个真实的案例:团队中有人本地用Gradle 7.6,CI用8.2,结果同一个Composable函数生成的@Composable注解的类文件签名差了一个空格,导致快照里的文本对齐偏移了1px。

解决方案:统一版本,锁定环境

方案其实不复杂,核心就三个字:锁版本。下面是我强推的做法:

推荐工具链:Gradle Wrapper + JDK Toolchain

第一步:统一Gradle版本。用Wrapper锁定,提交到Git。

# 生成wrapper
gradle wrapper --gradle-version 7.6.4

# 或者手动编辑 gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip

第二步:统一JDK。用Toolchain声明,这样开发机和CI都下载同一个JDK。

// build.gradle.kts
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
        vendor.set(JvmVendorSpec.AZUL)
    }
}

第三步:CI上强制使用Toolchain指定的JDK,而不是CI默认的JDK。

# GitHub Actions 示例
- name: Setup Java
  uses: actions/setup-java@v4
  with:
    java-version: '17'
    distribution: 'temurin'
注意: 即使使用了Toolchain,有些CI系统仍可能用系统默认JDK跑Gradle Daemon,导致编译时部分任务使用错误JDK。建议在CI脚本里显式设置 GRADLE_JDK_HOME 或 JAVA_HOME。

最佳实践:一个清单搞定

我建议每个项目建立一份环境兼容性清单:

  • 开发环境:JDK 17.0.12,Gradle 7.6.4
  • CI环境:JDK 17.0.12(用Toolchain强制覆盖),Gradle 7.6.4(用Wrapper)
  • 预提交检查:本地运行一次完整快照测试,再push
  • 只读缓存:CI开启Gradle远程缓存(如Build Cache),避免因本地缓存导致差异

下面这张图帮你理清整个链路:

JDK / Gradle 版本不兼容导致快照差异的链路 开发环境 JDK 11 / Gradle 7.4 CI 环境 JDK 17 / Gradle 8.0 环境配置不一致 Gradle 编译 (Task顺序 / 字节码) Gradle 编译 (Task顺序 / 字节码) 快照结果 A (布局 / 文本 / 图片) 快照结果 B (布局 / 文本 / 图片) 二进制对比失败 快照回归测试失败 Assert 差异 ≠ 代码逻辑错误 解决方案:统一 JDK Toolchain + Gradle Wrapper + 缓存一致策略 开发环境与CI环境保持相同的JDK大版本、Gradle版本、以及远程缓存配置

避坑指南

我曾经踩过一个很隐蔽的坑:CI上虽然用了Gradle Wrapper,但Gradle Daemon是用系统默认JDK启动的,导致Toolchain虽然声明了JDK 17,但编译时部分任务混用了JDK 11的类库。解决办法是在CI脚本里显式设置 ORG_GRADLE_PROJECT_javaHome 环境变量。

另外提醒一下:不只是快照测试,所有回归测试的稳定性都依赖环境一致。我建议把JDK和Gradle版本记入项目README,并在CI配置中增加版本检查步骤:

# 检查JDK版本
java -version 2>&1 | grep -i "openjdk version" | awk '{print $3}' | grep -q "17"
if [ $? -ne 0 ]; then
  echo "Error: JDK 17 required"
  exit 1
fi
要点总结:
  • 快照差异 ≠ 代码问题,优先排查环境版本
  • 使用Gradle Wrapper锁定Gradle版本
  • 使用Java Toolchain锁定JDK版本
  • CI上强制覆盖JDK_HOME,避免Daemon污染
  • 保持开发/CI/验证环境的Gradle缓存配置一致