Netty4.1.x的类冲突问题

Netty4.1.x的类冲突问题

间接问题

直接进主题。最近在搞一个大项目,来来回回涉及到几十个工程。其中有八个工程是新工程。部署的时候一直报错NoSuchMethodException:ByteToMessageDecoder.ensureNotSharable。看到这个异常基本上可以断定是类冲突问题,所以解决思路也应该先往排除相同依赖之类的方向上走。

目前我们这边的RPC使用的是Netty的4.1.7版本,而且确实也间接的使用到了ByteToMessageDecoder(业务解码器继承LengthFieldBasedFrameDecoderLengthFieldBasedFrameDecoder继承ByteToMessageDecoder)。查看4.1.7版本ByteToMessageDecoder的构造函数:

1
2
3
protected ByteToMessageDecoder() {
CodecUtil.ensureNotSharable(this);
}

并没有使用CodecUtilensureNotSharable方法,而是说没有ByteToMessageDecoderensureNotSharable方法。查询项目中也只有4.1.7版本的Netty在使用。非常奇怪。

继续看日志,发现异常是从Netty的4.1.11版本中的ByteToMessageDecoder抛出来的。现跑去看了一下4.1.11版本的ByteToMessageDecoder构造函数:

1
2
3
protected ByteToMessageDecoder() {
ensureNotSharable();
}

还是很奇怪,为啥项目中包括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
2
3
protected ByteToMessageDecoder() {
CodecUtil.ensureNotSharable(this);
}

继而调用了一下CodecUtil.ensureNotSharable(this);。OK,一切正常。

然后执行new Decoder2

1
2
3
protected ByteToMessageDecoder() {
ensureNotSharable();
}

这时候,字节码里要求我们执行ByteToMessageDecoder.ensureNotSharable方法,然后我们去找加载了的ByteToMessageDecoderensureNotSharable方法,这个时候我们实际上加载了的是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工程的快照包版本升级了一下,就不再出现这个问题了。

后记

通过这篇文章其实主要是提供两个思路,第一个是如果碰到了NoSuchMethodClassNotFound以及NoClassDefFound等等异常,基本上断定是一个包冲突问题,我们要从报错的类推导出到底依赖的项目中哪几个包产生了冲突,以及是哪几个地方把这些包依赖进来了。这里一般mvn dependency:tree >> tree.txt,会在本地项目里生成一个文件,可以从里面看到各种依赖关系。

如果我们发现快照包里没有的,打包的时候却被打进来了,或者快照包里有的,打包的时候没有被打进来,这里很有可能是出现了没有强制拉包的问题,可以开启-U或者升级快照包的方式解决。