背景:
- 微服务应用xxx.jar依赖
netty-all-4.1.25Final
,项目中使用通信工具async-http-client-2.0.31
内置耦合了io.netty
的几个同包名同类名的类;- Tomcat应用运行时报:
java.lang.NoSuchMethodError: io.netty.channel.DefaultChannelId.newInstance()Lio/netty/channel/DefaultChannelId;
。But netty-all包中是存在DefaultChannelId
类的,显然发生冲突了(该类加载的地方并非来自于netty-all-4.1.25Final.jar
==>后面分析类来源)- 微服务应用必须使用高版本的netty,固只能考虑升级
http-client
,很遗憾,http-client
从某个版本开始结构发生了变化,虽然已解耦了io.netty
,But某些类已经被删除了;- 两种方案:1)重构项目中使用的
http-client
,升级到高版本进行重构;2)保持netty-all-4.1.25Final
不变的同时让JVM加载到想要的类;
一、问题分析过程
- 问题:
pom.xml
文件依赖:
······
<dependency>
<groupId>org.asynchttpclient</groupId>
<artifactId>async-http-client</artifactId>
<version>2.0.31</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>
······
启动应用调用某依赖到netty-all.jar
的服务接口时报错:
java.lang.NoSuchMethodError: io.netty.channel.DefaultChannelId.newInstance()Lio/netty/channel/DefaultChannelId
附上http-client
工具包是如何嵌入netty
相关包的:
httpclient并没有把依赖netty
的部分当做依赖引入,而是拷贝过来内嵌在自己的工具包里面。关键是同package名同类名
,<exclude>
显然做不到,哪个依赖放在前面
显然是无效的,因为依赖的加载受Tomcat版本、操作系统的文件系统对文件的排列等因素的影响
2. 解决过程:
-
目的:让JVM加载到
netty-all
下的DefaultChannelId.class
而不是http-client
下的DefaultChannelId.class
-
maven出现同个jar包不同版本时,会遵从
路径最短
和第一声明
原则决定加载哪个包;对不同jar中存在同package名同className的情况时Maven将束手无策,取决于先加载哪个jar就先加载到那个class,另外一个jar的相同class将不会加载; -
Tomcat服务下JVM应用是如何加载
jar
和class
的:
加载顺序:
1. $java_home/lib 目录下的java核心api
2. $java_home/lib/ext 目录下的java扩展jar包
3. java -classpath/-Djava.class.path所指的目录下的类与jar包
4. $CATALINA_HOME/common目录下按照文件夹的顺序从上往下依次加载
5. $CATALINA_HOME/server目录下按照文件夹的顺序从上往下依次加载
6. $CATALINA_BASE/shared目录下按照文件夹的顺序从上往下依次加载
7. 我们的项目路径/WEB-INF/classes下的class文件
8. 我们的项目路径/WEB-INF/lib下的jar文件
在同一个文件夹下,jar包是按顺序从上到下依次加载(其实还受Tomcat版本和文件系统的影响,不一定是按字母顺序加载)!!!
由ClassLoader的双亲委托模式加载机制我们可以知道,假设两个包名和类名完全相同的class文件不再同一个jar包,如果一个class文件已经被加载java虚拟机里了,那么后面的相同的class文件就不会被加载了。
应用用的是tomcat7,加载lib
是按字母顺序从上到下加载jar
包,当然async-http-client
比netty-all
先加载,自然DefaultChannelId.class
来自于async-http-client.jar
,可以验证一下:
- 好了,知道的问题的本质,接下来该知道怎么解决了,个人想了几种方案:
1)项目中引入http-client
耦合的同名netty
高版本的class
文件,让类加载器优先从这里加载到。当然,如果你的项目还要打包成SDK给其他项目调用的时候,得三思这种方案了,不然又回到了解决http-client
嵌入netty
的死循环;
2)修改Tomcat下的context.xml
,指定类从哪个jar
包加载(未实验,感觉这种方式有点不雅):
<Context>
<!-- Default set of monitored resources. If one of these changes, the -->
<!-- web application will be reloaded. -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!--
<Manager pathname="" />
-->
<Resources>
<PreResources className="org.apache.catalina.webresources.FileResourceSet"
base="/home/webroot/ls-static-web/WEB-INF/lib/ls-aacustom-common-1.0-SNAPSHOT.jar"
webAppMount="/WEB-INF/lib/ls-aacustom-common-1.0-SNAPSHOT.jar" />
<PreResources className="org.apache.catalina.webresources.FileResourceSet"
base="/home/webroot/ls-static-web/WEB-INF/lib/ls-aacustom-static-1.0-SNAPSHOT.jar"
webAppMount="/WEB-INF/lib/ls-aacustom-static-1.0-SNAPSHOT.jar" />
</Resources>
</Context>
3)重构项目用到HttpClient
代码,升级到高版本并使用新的类替换;
作者:Jorce
参考文章:
[1]. tomcat加载jar包顺序
[2]. NoSuchMethodError报错和Tomcat的jar包加载顺序
[3]. Java中重名类冲突处理机制和Jar包加载顺序
[4]. 重新看待Jar包冲突问题及解决方案