PAYATHON 2026

Sonar Gradle task 因 SNAKE_CASE 属性缺失而失败,如何排查?

排障赵师傅

结论

现有日志还不能确认这是 SonarQube Gradle plugin 7.2.2 与 Jackson 2.21.x 的已知兼容性问题。

报错说明,运行中有代码尝试读取 com.fasterxml.jackson.databind.PropertyNamingStrategy.SNAKE_CASE,但当时加载的类没有这个属性。先找出访问旧 API 的调用方及其实际加载的 Jackson JAR,再决定修改代码、升级插件还是调整依赖。

可以优先检查两条线索,但现有日志尚不能证明哪一项是根因:

  • 构建使用了 dev.projektor.publish,失败前出现过 Projektor 报告链接,可检查它的报告发布逻辑和构建结束回调。
  • CI 通过 JAVA_TOOL_OPTIONS 注入了 /app/otel-xxx-extensions.jar,本地与 CI 的 JVM 启动环境可能不同。

为什么业务代码没有引用旧 API,仍会报错?

Gradle 构建时还会执行构建脚本、插件、任务监听器和外部注入的 Java agent。即使业务代码已经使用 PropertyNamingStrategies,这些组件仍可能访问:

PropertyNamingStrategy.SNAKE_CASE

No such property 这种报错形式提示,需要检查 Groovy 动态属性访问。具体的异常类型和调用位置,要通过完整异常栈确认。

依赖中没有低于 2.19 的 Jackson,也不能排除兼容性问题。某个组件可能仍在调用旧 API,而运行时解析到的版本已不再提供该字段;不同 classloader 也可能加载不同的 Jackson。

因此,除了应用依赖,还要检查 Gradle 插件依赖,以及扫描器或 agent 自身的加载环境。

第一步:取得完整异常栈,确认谁访问了 SNAKE_CASE

在 Vela 中保留原有镜像、postgres 用户和工作目录,执行:

./gradlew clean build sonar --full-stacktrace --info --console=plain

重点查看 Caused by,找到属性访问对应的调用链,再沿着调用链定位第一个属于项目脚本或第三方组件的栈帧,记录类名、方法名和行号。

最后出现的 > Task :sonar 不足以判断异常归属。任务完成后的回调、报告上传或构建结束逻辑也可能在这个阶段失败。Projektor 报告链接只说明相关逻辑执行过,不能证明后续异常由它引起。

如果 --info 提供的信息仍不够,再使用 --debug。调试日志可能包含认证信息,分享前应脱敏。

随后将构建和扫描拆开执行:

./gradlew clean build --full-stacktrace --info --console=plain
./gradlew sonar --full-stacktrace --info --console=plain

执行第二条命令前,保留第一条命令生成的编译产物和报告,不要再次运行 clean。

如果第一条命令就出现相同异常,应检查普通构建及其回调;如果只有第二条失败,再检查扫描阶段及其触发的插件逻辑。本地也应执行这两条命令,单独一次 build 成功并不能复现 CI 的完整流程。

第二步:分别检查应用和构建插件的依赖

先查看应用运行时解析到的 Jackson:

./gradlew dependencyInsight \
  --dependency jackson-databind \
  --configuration runtimeClasspath

如果异常涉及测试,再检查:

./gradlew dependencyInsight \
  --dependency jackson-databind \
  --configuration testRuntimeClasspath

然后检查构建脚本依赖:

./gradlew buildEnvironment

多模块项目应在发生错误的模块上执行相应任务,例如:

./gradlew :your-module:buildEnvironment

buildEnvironment 可以查看构建脚本 classpath,但无法完整展示插件自行创建的隔离 classloader、扫描器子进程或 Java agent 的内部依赖。即使依赖报告没有发现异常,也需要核实实际的类加载来源。

查看实际加载的是哪个 JAR

当前使用 Java 17,可以临时启用类加载日志。下面的命令保留已有的 JAVA_TOOL_OPTIONS,并追加日志参数:

JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS:+$JAVA_TOOL_OPTIONS }-Xlog:class+load=info" \
  ./gradlew sonar --full-stacktrace --console=plain \
  > classload.log 2>&1

检索目标类:

rg 'com\.fasterxml\.jackson\.databind\.PropertyNamingStrategy' classload.log

找到日志中的来源路径后,检查对应 JAR 的公开成员。将下面的路径替换为实际路径:

javap -classpath /path/to/jackson-databind.jar \
  com.fasterxml.jackson.databind.PropertyNamingStrategy

确认其中是否存在 SNAKE_CASE。如果日志涉及多个 JVM,或同名类被多次加载,需要结合异常所属进程和调用方确定对应的 JAR。

第三步:根据调用栈验证 Projektor 和 agent

如果调用栈指向 Projektor

在临时分支中移除:

id "dev.projektor.publish" version "9.0.0"

同时移除对应配置:

projektor {
    serverUrl = "https://projektor.dev.xxx.com"
}

然后重新执行原来的 clean build sonar。

如果异常消失,说明问题与 Projektor 的参与有关。接下来结合栈帧和实际加载的 Jackson,确认具体调用位置,并检查插件是否有适用的修复版本。仅凭报告链接,还不能认定 Projektor 9.0.0 存在已确认缺陷。

如果差异集中在 CI 的 Java agent

日志显示,JVM 启动时加载了:

-javaagent:/app/otel-xxx-extensions.jar

如果本地没有相同注入,可以在一次诊断构建中停用它作对照。确认 JAVA_TOOL_OPTIONS 中只有这项 agent 配置后,进入仓库、切换到相同用户,再执行:

env -u JAVA_TOOL_OPTIONS \
  ./gradlew clean build sonar --full-stacktrace --info --console=plain

如果该变量还包含其他必要参数,应只移除对应的 -javaagent 参数。

停用后构建成功,说明问题依赖该注入环境。具体是 agent 代码、类加载变化,还是它与其他组件的交互导致异常,仍需通过异常栈和类加载来源确认。

找到调用方后如何修复?

如果可以维护旧调用所在的代码,可将命名策略引用改为:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;

ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

如果调用来自第三方 Gradle 插件,优先选择包含相应修复、且适配当前构建环境的插件版本。没有可用修复时,可以临时停用相关功能或维护补丁。

确认冲突发生在哪个 classpath 后,再考虑调整该范围内的 Jackson 依赖。直接修改应用的 implementation 或全局强制 Jackson 版本,可能影响不到发生异常的插件 classloader,还可能引入新的依赖冲突。

Spring Boot 升级后开始失败,说明版本变化是复现条件之一,但还不足以确定哪个组件存在缺陷。需要比较升级前后的依赖解析结果。问题描述没有明确给出 3.5.13 本身的测试结果,因此还不能把它视为已确认的故障分界点。

日志中的其他警告应单独处理

现有证据不足以将这些警告与 SNAKE_CASE 异常关联起来,仍需分别处理。

日志已将 sonar.login 标记为弃用,应统一使用 sonar.token 或 SONAR_TOKEN,避免重复配置。

sonar.jacoco.reportPath 已不受支持,需要生成 JaCoCo XML 报告并配置路径。例如,在现有配置中补充:

tasks.named("jacocoTestReport") {
    dependsOn(tasks.named("test"))
    reports {
        xml.required = true
    }
}

sonarqube {
    properties {
        property "sonar.coverage.jacoco.xmlReportPaths",
                layout.buildDirectory
                        .file("reports/jacoco/test/jacocoTestReport.xml")
                        .get().asFile.absolutePath
    }
}

删除旧的 sonar.jacoco.reportPath,并在扫描前生成报告:

./gradlew clean build jacocoTestReport sonar

如果修改过 XML 报告的输出位置,属性值也要相应调整。

原配置中的 sonar.source 和 exclusions,如果分别用于指定源码和排除文件,应检查并改为 sonar.sources、sonar.exclusions。通常也可以优先使用 Gradle 插件从 source set 推导的源码范围。

对于 Unresolved imports/types 和 preview features 提示,应继续查看扫描器调试日志。修正这些配置有助于保证分析结果准确;定位本次失败,则应从完整异常栈中的旧 API 调用入手。

备注:内容仅供参考。