别被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进来,小编猜测有下面两个原因:

  1. 驱动包是需要相对独立的,而且这两个驱动都没有用依赖管理工具(比如:maven、gradle),所以不能使用常规的dependence方式

  2. 驱动包需要精简,体积小,而且需要做一些定制,所以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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值