别被NoSuchMethodError骗了
小编工作中尝尝被NoSuchxxx这类报错拦住,这里面最常见的莫过于NoSuchMethodError。程序所说的“No such meth”真的就是在类里面没有这个方法吗?不,100%的情况是jar包冲突了。程序只是告诉你它在当前这个jar包的类A中没有找到需要的方法,如果真的没有这个方法,那在编辑器中早就爆红了。问题来了,为什么这个bug能绕过编辑器的检查,知道运行时,才发现方法找不到呢?这就引出了今天我们接下来讨论的问题。
NoSuchMethodError不一定方法真的不存在
如果两个Jar包含相同的类,这个类的全路径相同,类名也相同,那对加载器来说,它很懵逼,因为它区分不了这两个类,只能是谁先被load,谁就会在jvm中,另一个只能被忽略。这导致一个问题,如果被加载的A中方法比A’的方法多,且被其它地方用到了,但是刚好A’被加载器加载了,那程序运行时就哭了,抛出异常:
Caused by: java.lang.NoSuchMethodError: org.bson.types.ObjectId.toHexString()Ljava/lang/String;
IDEA快捷键Shift + Shift搜索org.bson.types.ObjectId
类,发现
mongo-java-driver和ifxjdbc同时使用bson.types.ObjectId,而且他们都是把org.bson.types这个包下面的类copy到了各自的源码中,为什么不依赖,而是copy进来,小编猜测有下面两个原因:
-
驱动包是需要相对独立的,而且这两个驱动都没有用依赖管理工具(比如:maven、gradle),所以不能使用常规的dependence方式
-
驱动包需要精简,体积小,而且需要做一些定制,所以copy源码,在那基础上添加、删除一个方法
类名、类路径完全一样,反编译这两个类,发现ifjdbc.jar里面的ObjectId的确没有toHexString()这个方法。
可是编译的时候却没发现这个问题,小编猜测编译的时候,两个jar采取就近原则,所以各自使用各自jar中的类,没有任何问题。
两个Jar包含相同的类怎么办?
先比较两个类的异同,如果其中一个类A的方法能囊括另一个类A’,那我们人为干预jar在classpath中的加载顺序,让A所在的Jar优先被加载。如果两个类互相不兼容,那就只能造出一个A和A’方法的并集的类A’’,然后使用解压缩工具将两个jar中的class文件替换掉。
Jar在classpath中的顺序真的很重要吗?
根据jvm类加载机制,类全路径会作为类的唯一标识,classpath中类全路径相同的类,只有一个会被加载到jvm中,这也提醒我们,类和包的命名一定要考虑唯一性,这也是为什么我们公司都是用公司的域名来命名包。github开源项目都是用com.github.xxxx来命名包。
Linux和window环境是怎么排序jar的?
发现mongo-java-driver中的ObjectId相对ifxjdbc方法更全,那就想办法让classpath中mongo-java-driver.jar位于ifxjdbc.jar的前面。
先看linux上classpath的jar的顺序:
> ps -ef|grep /home/test1/test-web
33339 574953 1 0 Jul20 ? 01:24:31 /usr/bin/hadoop/software/java8/bin/java -Dfile.encodings=UTF-8 -Xms2048M -Xmx2048M -XX:MaxPermSize=1024M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -Dlog.dir=/home/test1/test-web/logs -Dconf.dir=/home/test1/test-web/conf -Xverify:none -cp :/home/test1/test-web/conf::/home/test1/test-web/lib/javax.inject-1.jar:/home/test1/test-web/lib/spring-cloud-starter-netflix-eureka-client-2.0.0.RELEASE.jar:/home/test1/test-web/lib/spring-cloud-starter-2.0.0.RELEASE.jar:/home/test1/test-web/lib/spring-boot-starter-2.0.4.RELEASE.jar:/home/test1/test-web/lib/spring-boot-2.0.4.RELEASE.jar:/home/test1/test-web/lib/spring-core-5.0.8.RELEASE.jar:/home/test1/test-web/lib/spring-jcl-5.0.8.RELEASE.jar:/home/test1/test-web/lib/spring-context-5.0.8.RELEASE.jar:/home/test1/test-web/lib/spring-aop-5.0.8.RELEASE.jar:/home/test1/test-web/lib/spring-beans-5.0.8.RELEASE.jar:/home/test1/test-web/lib/spring-expression-5.0.8.RELEASE.jar:/home/test1/test-web/lib/spring-boot-autoconfigure-2.0.4.RELEASE.jar:/home/test1/test-web/lib/spring-boot-starter-logging-2.0.4.RELEASE.jar:/home/test1/test-web/lib/logback-classic-1.2.3.jar:/home/test1/test-web/lib/logback-core-1.2.3.jar:/home/test1/test-web/lib/slf4j-api-1.7.25.jar:/home/test1/test-web/lib/log4j-to-slf4j-2.10.0.jar:/home/test1/test-web/lib/log4j-api-2.10.0.jar:/home/test1/test-web/lib/jul-to-slf4j-1.7.25.jar:/home/test1/test-web/lib/javax.annotation-api-1.3.2.jar:/home/test1/test-web/lib/snakeyaml-1.19.jar:/home/test1/test-web/lib/spring-cloud-context-2.0.0.RELEASE.jar:/data2 ...省略
-cp参数中即是jar的顺序,明显ifxjdbc在mongo-java-driver前面,经过进一步追查,这个-cp参数是下面的代码shell生成的:
# 将jar包放入classpath中
function addEachJarInDir() {
if [[ -d "${1}" ]];
then
for jar in $(find -L "${1}" -maxdepth 1 -name '*.jar');
do
APP_CLASSPATH="${APP_CLASSPATH}:${jar}"
done
fi
}
也就是说jar包顺序没有经过排序顺利,只是把${1}
指定的路径中的jar包通过find全列出来,通过:拼接了。那${1}
下面的顺序又是谁决定的?
copy到sublime text中观察发现,貌似就是maven依赖的顺序
OK !那就把mongo-java-driver移动到ifxjdbc前面,Linux上重启运行没问题了。
Window上我们Idea开发环境又会怎么样呢?
在IDEA上运行main,第一行日志就打印了运行命令信息
D:\Java\jdk8_x64\jre\bin\java.exe -javaagent:D:\IDEA-U\ch-0\182.4505.22\lib\idea_rt.jar=50598:D:\IDEA-U\ch-0\182.4505.22\bin -Dfile.encoding=UTF-8 -classpath C:\Users\xxxxx\AppData\Local\Temp\classpath381627355.jar com.xxx.test.web.Application
2020-08-03 17:09:03 INFO c.q.b.f.spring.startup.OnStartup -
因为IDEA运行时,如果项目依赖jar太多,会报“common line too long”异常,所以选择了JAR Manifest方式
简单来说,就是IDEA会将运行需要的classpath打成一个空jar包,然后将那个长长的classpath字符串放到MANIFEST.MF文件中,这样命令行只需要将classpath指定为了一个jar就好了-classpath C:\Users\xxxxx\AppData\Local\Temp\classpath381627355.jar
那我们来看看MANIFEST.MF文件内容,将其copy到sublime text文本编辑器中,发现顺序的规律貌似也是dependence的顺序,OK!那就不用考虑兼容性了。本地启动,运行OK!
结论
“路径相同,且类名也相同”的类,出现在“2个以上”的Jar中,运行时,“有可能”会出现NoSuchMethodError。