Netty4.1.x的类冲突问题
间接问题
直接进主题。最近在搞一个大项目,来来回回涉及到几十个工程。其中有八个工程是新工程。部署的时候一直报错NoSuchMethodException:ByteToMessageDecoder.ensureNotSharable
。看到这个异常基本上可以断定是类冲突问题,所以解决思路也应该先往排除相同依赖之类的方向上走。
目前我们这边的RPC使用的是Netty的4.1.7版本,而且确实也间接的使用到了ByteToMessageDecoder
(业务解码器继承LengthFieldBasedFrameDecoder
,LengthFieldBasedFrameDecoder
继承ByteToMessageDecoder
)。查看4.1.7版本ByteToMessageDecoder
的构造函数:
1 | protected ByteToMessageDecoder() { |
并没有使用CodecUtil
的ensureNotSharable
方法,而是说没有ByteToMessageDecoder
的ensureNotSharable
方法。查询项目中也只有4.1.7版本的Netty在使用。非常奇怪。
继续看日志,发现异常是从Netty的4.1.11版本中的ByteToMessageDecoder
抛出来的。现跑去看了一下4.1.11版本的ByteToMessageDecoder
构造函数:
1 | protected ByteToMessageDecoder() { |
还是很奇怪,为啥项目中包括mvn
的依赖树工具都没有找到Netty 4.1.11版本(但是有3.x版本,这也误导了我一下,一开始以为是3.x和4.x版本的ByteToMessageDecoder
冲突了,后来发现3.x版本没有这个类),但是错误居然会从4.1.11版本中的ByteToMessageDecoder
抛出来。
这个只是间接原因,但是还是列出来并且讲一下为啥会冲突。从表现来看,我们只能“假设我们用到了Netty 4.1.11版本”。如果用了,为啥会产生这个问题呢?
本篇主要是给大家看下解决问题的思路,并非讲解类加载器,所以我们这里不细说类加载的东西,我们就把假设所有的已经被加载了的类被放在一个盒子里,我们需要的时候就去取一个出来用。往这个盒子里放类加载器的规则就是:如果盒子里目前没有这个类,就加载了以后放进去。取的规则也很简单:如果我想要的类已经有了,就直接从盒子里取出来用。
规则定完了,我们看看为啥会出现上述的问题。首先,我们需要用到我们自己的业务解码器,这个解码器见解继承了ByteToMessageDecoder
,那么我们使用的是Netty 4.1.7版本,所以就把4.1.7版本的类加载了并且放到了我们的盒子里。
然后我们之前假设,我们的工程中也用到了4.1.11版本,可能是其他工程也有一个解码器,继承的是4.1.11版本的那个ByteToMessageDecoder
:
父类ByteToMessageDecoder的版本 | 4.1.7 | 4.1.11 | ||
---|---|---|---|---|
子类 | Decoder1 | Decoder2 | Decoder2 |
加载Decoder2
的时候,也需要加载他的父类,及4.1.11版本的ByteToMessageDecoder
。这时候,会先去盒子里捞一把,发现,咦,这个ByteToMessageDecoder
已经存在了!这个时候,就认为4.1.11版本的ByteToMessageDecoder
已经被加载了,就返回了。
然后我们开始执行new Decoder1
(执行new Decoder1
会执行其父类的new
方法,这个地方不细说了),没有任何问题,因为Decoder1
本来就是继承的4.1.7版本的ByteToMessageDecoder
,执行4.1.7版本的class的字节码:
1 | protected ByteToMessageDecoder() { |
继而调用了一下CodecUtil.ensureNotSharable(this);
。OK,一切正常。
然后执行new Decoder2
:
1 | protected ByteToMessageDecoder() { |
这时候,字节码里要求我们执行ByteToMessageDecoder.ensureNotSharable
方法,然后我们去找加载了的ByteToMessageDecoder
的ensureNotSharable
方法,这个时候我们实际上加载了的是4.1.7版本的ByteToMessageDecoder
,在这个4.1.7版本里,ensureNotSharable
方法并不属于ByteToMessageDecoder
!这时候,理所当然我们就会报错了,这也是为什么错误是从4.1.11版本中抛出来的。
直接原因
说了4.1.7和4.1.11类冲突问题,还有个奇怪的地方——之前我们是假设用到,但是实际上在本地的mvn的dependency tree里完全看不到4.1.11版本。
这个问题的排查过程我就不细说了,直接说结果吧。
我们直接登录到了打包服务器上,mvn dependency tree了一把,居然在打包服务器上发现了4.1.11的依赖。那么现在问题就简化为,为什么本地命名没有依赖4.1.11的情况下,打包服务器上一直有依赖4.1.11。
最后定位是这样的,我们的工程A依赖一个工程B的RPC接口,B工程由于一些特殊需要,把Netty的4.1.11打在包里暴露出来了,依赖关系:
A --> B --> Netty 4.1.11
但是我们明明在本地看不到B依赖了4.1.11,为什么打包服务器上可以看到依赖了Netty 4.1.11。毕竟我们是部署在服务器上,如果打包服务器上有4.1.11,那么还是会把4.1.11打入到项目里。
这个问题其实基本可以断定是maven的拉包问题,maven有一个强制拉包的选项-U
,maven默认是没有开启的,及如果发现有一个快照包在打包服务器上,就直接用这个快照包,不去中央仓库拉取最新的快照包。我们的测试环境的打包服务器是没有加这个参数的,原因是测试环境打包频繁,一直强制更新非常浪费带宽。
我们去询问了一下B工程的负责人,之前确实打了一个快照版本的包依赖了Netty 4.1.11。我们直接把B工程的快照包版本升级了一下,就不再出现这个问题了。
后记
通过这篇文章其实主要是提供两个思路,第一个是如果碰到了NoSuchMethod
、ClassNotFound
以及NoClassDefFound
等等异常,基本上断定是一个包冲突问题,我们要从报错的类推导出到底依赖的项目中哪几个包产生了冲突,以及是哪几个地方把这些包依赖进来了。这里一般mvn dependency:tree >> tree.txt
,会在本地项目里生成一个文件,可以从里面看到各种依赖关系。
如果我们发现快照包里没有的,打包的时候却被打进来了,或者快照包里有的,打包的时候没有被打进来,这里很有可能是出现了没有强制拉包的问题,可以开启-U
或者升级快照包的方式解决。