Sonar Gradle 插件访问 getInputStream 受保护方法失败如何排查?
这类错误应优先从构建插件运行时的二进制兼容性入手排查。直接触发失败的是 GzipCompressorInputStream 调用了运行时访问权限为 protected 的 AbstractStreamBuilder.getInputStream(),但调用位置不满足访问条件。
现有材料不足以确定哪个插件引入了冲突,也不能仅凭 build.gradle 中的声明认定实际加载的 JAR 版本。建议先移除手工添加的 Commons 依赖覆盖,做一次对照,再确认 CI 的依赖解析结果和类加载来源。
为什么应先检查 commons-io 和 commons-compress?
在 commons-io:2.16.1 的源码中,该方法声明为:
protected InputStream getInputStream() throws IOException
如果实际运行的调用方是按该方法可公开访问的 API 编译的,运行时加载到这个版本就可能发生访问错误。该方法在较新的 Commons IO API 中为 public,因此版本变化与当前异常直接相关。Commons IO 2.16.1 源码、Commons IO API
异常还显示,这两个类由不同的 InstrumentingVisitableURLClassLoader 实例加载。这是检查插件类加载边界的线索,但不同 ClassLoader 本身不能证明配置错误,也不能据此断言 commons-compress:1.26.2 与 commons-io:2.16.1 的组合必然触发异常。
配置中的注释也不代表实际行为:
because 'Required for Sonar 6.3+ compatibility'只是依赖选择原因的说明,不能作为兼容性依据。- 普通版本约束和直接依赖声明不等同于
force,解析结果仍可能选择其他版本。 buildscript中的规则不能当作覆盖所有插件内部 ClassLoader 的全局规则。Gradle 依赖约束说明
先移除手工覆盖,做一次对照
先删除针对以下两个模块的直接 classpath 声明和对应的 constraints 条目:
commons-io:commons-io
org.apache.commons:commons-compress
保留原有仓库、其他构建依赖及 plugins 配置。例如,buildscript 可以先缩减为:
buildscript {
repositories {
maven {
url "<myProvateRepo>"
}
mavenCentral()
}
dependencies {
classpath 'org.yaml:snakeyaml:2.6'
classpath 'org.flywaydb:flyway-database-oracle:12.1.0'
}
}
这份对照配置用于验证手工覆盖是否参与了冲突,并不代表已经确认所有插件的兼容版本。如果移除后恢复正常,应进一步记录实际解析的版本,再决定是否需要显式约束。
对比本地和 CI 的构建环境
在相同提交上,分别执行:
./gradlew --version
./gradlew buildEnvironment
重点比较 Gradle 实际使用的 JVM,以及依赖报告中的 commons-io、commons-compress 和引入它们的依赖路径。多模块项目还应检查应用 Sonar 插件的对应项目,例如 ./gradlew :app:buildEnvironment,其中 :app 需要替换为实际项目路径。
buildEnvironment 用于查看构建脚本的 classpath 依赖;普通的 dependencies --configuration runtimeClasspath 主要反映应用运行依赖,不能替代插件依赖检查。Gradle 依赖查看指南
还应比较两端的 GRADLE_USER_HOME、用户级 init.gradle 或 init.d 脚本、插件仓库设置及企业初始化配置。本地执行成功只能说明两端存在值得比较的差异,不能直接证明 CI 缓存损坏。
确认报错时真正加载的 JAR
如果依赖报告无法解释错误,可以在支持统一 JVM 日志的 JDK 上临时开启类加载日志:
export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS:-} -Xlog:class+load=info:file=sonar-classload-%p.log"
随后执行原有的 Sonar 命令,保留认证和 Pull Request 参数,并追加 --stacktrace --info。日志文件使用 %p 区分不同的 JVM 进程。
查找两个类的加载记录:
grep -E 'AbstractStreamBuilder|GzipCompressorInputStream' sonar-classload-*.log
根据日志中的实际路径,检查 Commons IO JAR 的方法声明:
javap -classpath /actual/path/commons-io.jar -p \
org.apache.commons.io.build.AbstractStreamBuilder
将 /actual/path/commons-io.jar 替换为日志中确认的路径。如果依赖报告显示一个版本,实际加载路径却指向另一个版本,应沿该路径检查其他插件、父级 ClassLoader 或构建初始化配置。仅查看缓存目录中有哪些 JAR,无法判断运行时使用了哪一个。
确认来源后再调整版本
修复时应针对实际引入冲突的依赖路径:
- 如果旧 Commons IO 来自手工
buildscript声明,移除覆盖,或按调用方的实际需要调整版本。 - 如果来自其他构建插件,检查该插件的依赖和兼容要求,必要时升级插件,并重新验证 Sonar 任务。
- 如果来自企业初始化脚本或仓库配置,修正对应规则,并对比本地与 CI 下载的 JAR 校验值。
当前信息不足以给出一个保证适用于这些插件的固定版本组合,也不能认定 Gradle 9.3.1 或 9.4.0 就是根因。
反复清缓存不会改变依赖声明和类加载关系;如果每次仍解析出同样的冲突组合,错误就会重现。排查期间还应查看 Sonar 步骤自身的退出状态。continue-on-error: true 允许工作流继续运行,工作流完成并不代表分析成功。
备注:内容仅供参考。
