记录一次因JVM加载类顺序引起的 NoSuchMethodException

2 11月

事故起因

项目上有一个需求,需要通过API的方式,滚动重启部署在k8s上的服务。

解决方案也挺简单的,通过调用k8s 的API service 就能解决,也有现成的客户端框架kubernetes-client/java。

整个实现机制可以参考k8s 的API Service介绍

然而整套逻辑写下来本地测试完全没有问题,一部署到k8s pod里问题就来了,报错 NoSuchMethodException。

从错误日志可以看出是一个 com.google.gson.stream.JsonWriter 没有 jsonValue(String)这个方法。

定位问题


第一反应,gson包冲突了。

赶紧打印了依赖树

确实有一个gson的依赖,一看版本,再去看对应的类,诶?!这个方法,是有的啊!

这就很诡异了。

之后尝试了各种方案:

排除 exclude 有可能依赖的低版本gson,不行

强制指定依赖高版本gson,不行

这里已经有点崩溃了,本地完全没有问题,一到服务器上问题就来了!此时半天已经没有了。


迫于开发压力,我们尝试把gson的源码复制到项目源码里,这样就算有依赖冲突,按照优先顺序,总应该先调用项目里面的类吧!

说干就干,我们复制了一份JsonWriter到源码里,保持了相同的包名,然后本地测试没有问题之后,再一次打包编译生成镜像,部署到k8s上。

然鹅,还是一样的问题,NoSuchMethodException。

这是什么情况?不应该啊?!!!

此时我非常好奇这个服务器上用的JsonWriter是哪儿来的了。因为一天已经接近尾声了。

k8s 定位问题

这里用到了阿里的arthas工具。

先下载工具

然后因为jdk版本的原因,必须把jdk整套复制到container里

然后进入container中

查看java使用的pid

这里可以看到pid是 13

启动arthas

先找到AppClassLoader的hashcode,比如你查看一个自己的静态类,或者主类都行,后面就有类加载器的hashcode

然后用这个类加载器加载目标类JsonWriter

之后就有类的信息了,也可以用上面的类扫描命令查看 sc

不看不知道,一看吓一跳

code-source 居然路径是 /opt/lib/gson.2.2.4.jar

这个是什么概念?

要知道我们项目gradle编译后的jar包只应该有2个,要么是app-SNAPSHOT.jar 要么是 app-dependencies.jar。前者是项目的源码,后者是所有的依赖。

结果这个JsonWriter居然是单独出现的,这就只能说明一点,这个jar包,是在container里的!

最后发现是项目docker file引用的基础镜像是公司其他项目组做的,他们在/opt下手动放了这个jar包。

那为什么手动放个jar包就会出问题呢?

原因分析

如果是项目内的依赖问题,是可以通过配置解决依赖冲突或者类加载顺序的。

但是这个是和项目代码并列的jar包,都是放在classpath下的。

就是说A.jar包里有一个JsonWriter,B.jar包里也有一个JsonWriter。那么加载jar包的顺序是自然顺序。

然后根据JVM类加载机制的逻辑,对已经加载过一次的类,不会再加载第二次。所以导致类永远都是低版本的JsonWriter在使用!

也就是说本地永远是对的,一上服务器,就会出现这样的错误。

解决方案

在docker file里手动删掉这个jar就一切恢复如初啦!

 

发表评论

邮箱地址不会被公开。