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 调用入手。
备注:内容仅供参考。
