NoSuchMethodError、NoClassDefFoundError的常见原因和通用解决方式

2023-05-16

目录

环境信息

问题描述

错误分析

解决方法

常见原因

1.第三方包,作用域不对导致应用没导入该包

2.编译时和运行时使用的版本不一样

3.JDK版本不一样

4.多个同路径、同名的类

        1.代码复制场景

        2.代码移动场景

排查步骤

附录

NoClassDefFoundError

Maven仲裁机制:

JVM类加载机制


环境信息

        Spring Boot:2.0.8.RELEASE

        Spring Boot内置的tomcat:tomcat-embed-core 8.5.37

问题描述

        测试环境,部署打版之后,应用成功启动。可是在收到部分请求的时候,后端报错了,查看日志,发现出现了java.lang.NoSuchMethodError,具体是:

Caused by: java.lang.NoSuchMethodError: com.xxx.utils.IdMakerUtil.getInstance(JJJ)Lcom/x/utilxxs/IdMakerUtil;
    at com.xxx.framework.idgenerator.SnowflakeIdGenerator.<init>(SnowflakeIdGenerator.java:18)
    at com.xxx.utils.IdUtil.<clinit>(IdUtil.java:19)  at com.xxx.framework.web.interceptor.WebRequestBodyAdvice.afterBodyRead(WebRequestBodyAdvice.java:63)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyAdviceChain.afterBodyRead(RequestResponseBodyAdviceChain.java:100)
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:208)
    at 

错误分析

          java.lang.NoSuchMethodError是一种运行时异常(runtime error),发生于一个方法,在编译的时候是存在的,在程序运行时不存在。java的垃圾处理器(GC)无法回收对象创建时申请的空间,有可能会造成OutOfMemoryError。

        在这次报错中,提示了找不到的方法是

com.xxx.utils.IdMakerUtil.getInstance(JJJ)Lcom/xxx/utils/IdMakerUtil;

类全路径是com.xxx.utils.IdMakerUtil,方法名是getInstance,(JJJ)代表该方法有三个参数,发生于com/xxx/utils/IdMakerUtil这个类。

        打开本地项目,应用正常启动,请求也能正常放回,不会报NoSuchMethodError。

但是仔细查看本地的IdMakerUtil.getInstance(),发现是个无参方法,和报错提示里使用的三个入参不一样,所以提示了NoSuchMethodError。本地的IdMakerUtil和测试环境的运行jar包里的IdMakerUtil是一样的,那么问题可能出现在调用该方法(IdMakerUtil.getInstance(x,x,x))的地方,即日志里提示的:

com.xxx.framework.idgenerator.SnowflakeIdGenerator.<init>(SnowflakeIdGenerator.java:18)

        仔细对比本地和运行jar包里的SnowflakeIdGenerator这个类,在A依赖包里,发现是一样的。但是,并没有在init初始化(构造方法里)调用IdMakerUtil.getInstance(x,x,x)

        本地和运行jar包里的SnowflakeIdGenerator:

        

        代码都一样,但是报错了,重点就是运行jar包里SnowflakeIdGenerator的构造方法里,调用了三个入参的IdMakerUtil.getInstance(x,x,x),和本地代码执行逻辑不一样

        由此可以推断,运行jar包使用的SnowflakeIdGenerator这个类,不是在A依赖包里的,而是另一个同路径、同名的类(com.xxx.framework.idgenerator.SnowflakeIdGenerator)!涉及到了JVM类加载机制,同路径、同名的类,只能加载一个。

        此时,想解决问题得找到实际使用的该SnowflakeIdGenerator类是在哪个jar包里,这时可以通过引入Arthas来查看。Arthas的sc命令,可以用来查看 JVM 已加载的类信息。具体的使用可以见官网。

sc | arthas (aliyun.com)

[arthas@2188]$ sc com.xxx.framework.idgenerator.SnowflakeIdGenerator
com.xxx.framework.idgenerator.SnowflakeIdGenerator
Affect(row-cnt:1) cost in 34 ms.
[arthas@2188]$ sc -d com.xxx.framework.idgenerator.SnowflakeIdGenerator
 class-info        com.xxx.framework.idgenerator.SnowflakeIdGenerator       
 code-source       file:/home/p05_dev/tfb-whole-biz-service-app/tfb-whole-biz-service-app-6.0.1-SNAPSHOT.jar!/BOOT-INF/lib/e-module-2.0.0-SNAPSHOT.jar!/
 name              com.xxx.framework.idgenerator.SnowflakeIdGenerator       
 isInterface      false
 isAnnotation      false                          
 isEnum            false                    
 isAnonymousClass  false
 isArray           false
 isLocalClass      false
 isMemberClass     false
 isPrimitive       false
 isSynthetic       false
 simple-name       SnowflakeIdGenerator
 modifier          public              
 annotation             
 interfaces             
 super-class       +-com.xxx.framework.idgenerator.AbstractIdGenerator      
                     +-java.lang.Object
 class-loader      +-org.springframework.boot.loader.LaunchedURLClassLoader@20a14b55
                     +-sun.misc.Launcher$AppClassLoader@18b4aac2                    
                       +-sun.misc.Launcher$ExtClassLoader@149b8519                  
 classLoaderHash   20a14b55            

Affect(row-cnt:1) cost in 53 ms.
[arthas@2188]$ 

        可以看到,运行时使用的SnowflakeIdGenerator是在e-module-2.0.0-SNAPSHOT.jar包里,检查本地的代码,该包的源码里没有SnowflakeIdGenerator这个类,查看git历史记录发现,该类被移动位置了,从e-module移动到了A模块。但是e-module包没有发布到maven仓库里,测试环境打版的时候使用了旧的包。

解决方法

       发布e-module包到仓库里:mvn clean deploy

注意要先clean 再 deploy,其他同事应该是没有deploy,或者没先clean直接deploy导致发布失败。

        备注:公司是使用Jenkins来打版的,只是utils、framework框架基础包仅限于几个人修改,不开放给其他同事,修改了代码之后,需要手动在本地发布jar包到maven仓库。这个也是本次测试环境事故的一个原因。

        之前一个同事将SnowflakeIdGenerator从e-module包,移动到A包,并删除了构造函数里调用IdMakerUtil.getInstance地方,但是忘了将e-module包deploy到maven仓库,只将A包deploy上去。

常见原因

1.第三方包,作用域不对导致应用没导入该包

直接现象:解压运行时的jar包,找不到该方法所在的jar包。找不到class文件。

该方法位于第三方包内,而maven配置文件里,该包的作用域(scope)是provided时,程序打出的jar包是不包含该包的。

这里要理解scope里的compile、provided、runtime、test、system的区别

1.compile:(默认),编译、

2.provided:和compile很像,但是要求JDK或者应用所在的运行容器(比如tomcat、jetty)中包含该jar包。

3.runtime:运行时。意味着该jar包在编译期间不需要,但是在运行、测试期间需要。maven会在运行、测试的时候,将该jar包加入classpath里,编译的时候不会。

4.test:测试时。意味着该jar包只用于执行测试案例(src/test/java)的时候用到,比如测试包如JUnit,或者测试时要用的依赖包如IO包。

5.system:和provided很像,但是要求我们要手动提供该jar包,而永远不会去从maven仓库里查找该包。

6.import:用于jar类型(packaging)是pom,而且是在<dependencyManagement>域里。一般用于jar包管理里,用于声明会用到的依赖及其版本,编译、打包的时候不会真正的导入该包。其子包可以直接引入该包而不用指定版本号,这时才会真正的导入该包。

2.编译时和运行时使用的版本不一样

直接现象:解压运行时的jar包,找得到该方法所在的jar包。查看该jar包里的该方法,发现方法的参数、返回值等不一样。class版本不一样。

该方法所在的jar包,编译期间用的版本和运行时使用的版本不一样。

常见于公司内部的依赖包代码有更新,可是未发布到maven仓库,或者发布的时候没有先clean导致发布失败,造成线上环境使用的依赖包还是旧的依赖包。应该使用命令:mvn clean deploy部署

或者依赖里没指定jar包的具体版本号,间接依赖使用了旧的版本。

3.JDK版本不一样

直接现象:class版本不一样。

编译和运行使用不同版本的JDK,一般是本地编译的JDK版本比较高,线上环境运行的JDK版本比较低。

4.多个同路径、同名的类

直接现象:在提示的类路径(package)、类名里找得到指定的方法,而且方法的参数、返回值等 一模一样,可是还是提示NoSuchMethodError。

这时候可以换个思路来思考:既然在这个类里的这个方法是存在的,那么是不是运行的程序使用的根本不是该类?!

常见原因:

        1.代码复制场景

比如根据开源版本定制开发,添加了自定义内容,可是类的路径没有变,只是打成了新的jar包名。同时,还引入了开源版本包。JVM类加载器对于同一个类只会加载一次,实际加载的类和实际环境有关:Jar包依赖的路径、文件加载顺序等,和操作系统、环境里jar包的具体路径等有关,很难排查。

        2.代码移动场景

比如一个类,原本是在Ajar包里,由于重构、功能调整等原因,移动到了Bjar包里,并且改了方法的参数或返回值。部署的时候,Ajar包没有成功发布、打到运行jar包里,Bjar包成功发布并打到运行jar包里。这时候,Ajar包和Bjar包都存在该类(同名、同路径),只是两个类里的方法不一样。运行的时候,可能因为JVM类加载器加载顺序的不同,先加载了Ajar包里的类,这时候,就会出现NoSuchMethodError异常。

这种情况,是很难排查的,因为查看本地代码的时候,根据类路径、名称找到的该类是在Bjar包里的(因为A包里的该类已经删除掉,看不到了),而解压运行jar包里的Bjar包,会发现该类里的方法是存在的。而查找问题的人如果不是移动该类的开发人员,是很难知道A包里也可能有该类,运行的时候使用的是A包里的类(这时候,如果没有抛出NoSuchMethodError异常,即没有改方法参数,只是改了里面的逻辑,那么更难意识到这里出现了问题!!!)。

排查步骤

1.解压运行jar包,查看NoSuchMethodError提示的报错类,看该类是否有该方法(参数也一样);

2.如果没有该方法(参数也一致),如果该类是公司内部开发的类,确认环境打版用的分支里是否提示没有的方法。如果有,那么是打版问题,将该类所在的包打版上去(根据实际情况,是手动deploy到maven仓库还是直接Jenkins打版、等等);

3.如果没有该方法(参数也一致),如果该类是第三方包里的类,确认下运行jar包里该类对应的jar包版本是否和本地是一样的(即是否是我们想要的版本)。如果版本不一致,那么分析运行环境里使用到该版本的原因(Maven冲突、仲裁等等,比如间接依赖,可以看附录部分);

4.如果该方法(参数也一致),那么查看异常日志调用该方法的地方(如本次事故里的SnowflakeIdGenerator.<init>构造方法),和环境打版用的分支里进行对比。如果代码不一致,那么进入2、3步骤。如果代码一致,但是和实际运行时调用的该方法(如此次的IdMakerUtil.getInstance(JJJ))不一致,那么可以确定实际运行时用的SnowflakeIdGenerator类和我们代码里的SnowflakeIdGenerator类不是同一个。可以通过Arthas的sc实时监控查看使用的具体信息(参考错误分析里的做法)

附录

NoClassDefFoundError

Caused by: java.lang.NoSuchMethodError: com.xxx.utils.IdMakerUtil.getInstance(JJJ)Lcom/x/utilxxs/IdMakerUtil;
    at com.xxx.framework.idgenerator.SnowflakeIdGenerator.<init>(SnowflakeIdGenerator.java:18)
    at com.xxx.utils.IdUtil.<clinit>(IdUtil.java:19)               

        分析文章开头的这个错误日志,可以看到执行IdUtil.<clinit>时报错了,即IdUtil类加载失败了。

        <clinit>方法,是类初始化过程中,主要是对静态(static)成员变量、静态语句块(static{})进行初始化的操作。如果执行失败了,那么类加载、初始化就失败了。JVM里不会有该类,后续代码如果使用到该类,会抛出NoClassDefFoundError异常。

        所以,第二次访问该接口的时候,会抛出不一样的异常(java.lang.NoClassDefFoundError: Could not initialize class com.xxx.framework.utils.IdUtil):

org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: Could not initialize class com.xxx.framework.utils.IdUtil
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1006)
    ...
    at java.lang.Thread.run(Unknown Source)
Caused by: java.lang.NoClassDefFoundError: Could not initialize class com.xxx.framework.utils.IdUtil
    at com.xxx.interceptor.BizLogRequestBodyHandler.wrapperPubOperUserLogModel(BizLogRequestBodyHandler.java:103)
    at com.xxx.interceptor.BizLogRequestBodyHandler.doHandle(BizLogRequestBodyHandler.java:92)
    at com.xxx.framework.web.interceptor.WebRequestBodyAdvice.afterBodyRead(WebRequestBodyAdvice.java:63)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyAdviceChain.afterBodyRead(RequestResponseBodyAdviceChain.java:100)
    ...
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)

        如果遇到了java.lang.NoClassDefFoundError: Could not initialize class这个异常,根据提示初始化(initialize)失败了,要在之前的日志里查找先关的报错日志,看下有没有提示信息。一般是执行static静态域、代码块失败了,要根据具体的日志看下是因为什么失败。

Maven仲裁机制:

安全同学讲Maven间接依赖场景的仲裁机制-阿里云开发者社区 (aliyun.com)

Maven仲裁机制原则

1.依赖竞争时,越靠近主干的越优先。

2.单颗树在依赖在竞争时(dependencies)(注意:不是dependencyManagement里的dependencies):

当deep=1,即直接依赖。同级是靠后优先。

当deep>1,即间接依赖。同级是靠前优先。

3.单颗树在依赖管理在竞争时(注意:是dependencyManagement里的dependencies)是靠前优先的。

4.maven里最重要的2个关系,分别是继承关系和依赖关系。我们所有的规律都应该只从这2个关系入手。

下图中分别是2个子pom文件(方块代表依赖的节点,A-1 表示A这个节点使用的是1版本,字母代表节点,数字代表版本)。

左边这个子pom生成的树依赖了 D-1,D-2和D-5。满足依赖竞争原则1,即越靠近树的左侧越优先的原则,所以D-5会竞争成功。

但是B-1和B-2同时都位于树的同一深度,并且深度为1,由于B-2更加靠后,所以B-2会竞争成功。

右边的子pom生成的树依赖了 D-1和D-2,并且位于同一深度,但由于D-1和D-2是属于间接依赖的范围,deep大于1,所以是靠前优先,那么也就是D-1会竞争成功。

JVM类加载机制

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

NoSuchMethodError、NoClassDefFoundError的常见原因和通用解决方式 的相关文章

随机推荐