PAYATHON 2026

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,无法判断运行时使用了哪一个。

确认来源后再调整版本

修复时应针对实际引入冲突的依赖路径:

  1. 如果旧 Commons IO 来自手工 buildscript 声明,移除覆盖,或按调用方的实际需要调整版本。
  2. 如果来自其他构建插件,检查该插件的依赖和兼容要求,必要时升级插件,并重新验证 Sonar 任务。
  3. 如果来自企业初始化脚本或仓库配置,修正对应规则,并对比本地与 CI 下载的 JAR 校验值。

当前信息不足以给出一个保证适用于这些插件的固定版本组合,也不能认定 Gradle 9.3.1 或 9.4.0 就是根因。

反复清缓存不会改变依赖声明和类加载关系;如果每次仍解析出同样的冲突组合,错误就会重现。排查期间还应查看 Sonar 步骤自身的退出状态。continue-on-error: true 允许工作流继续运行,工作流完成并不代表分析成功。

备注:内容仅供参考。