系统稳定性—NoSuchMethodError常见原因及解决方法
当应用程序试图调用类(静态或实例)的指定方法,而该类已不再具有该方法的定义时,就会抛出java.lang.NoSuchMethodError错误。简单地说,就是同一个Class有多个版本的实现,并且在运行时调用了缺少方法的那个版本。
一、产生原因
在实际生产系统中,我们主要关注运行时抛出的NoSuchMethodError错误,该错误轻则导致程序异常终止,严重时甚至会产生不可预知的程序结果,比如支付服务执行异常,实际支付已完成,却向用户返回支付失败。
其中有以下两种情况:
1.1 编译时存在方法,运行时找不到
当编译Java代码时,Java编译器会检查类的所有方法,如果某个方法不存在或者签名不匹配,就会编译失败。但是在运行时,当代码调用一个不存在的方法时,就会生成NoSuchMethodError异常。
例如,下面的代码就会产生NoSuchMethodError异常:
public class Test {
public static void main(String[] args) {
int sum = add(1, 2, 3);
System.out.println(sum);
}
public static int add(int a, int b) {
return a + b;
}
}
在该代码中,我们在main方法中调用了add方法,并传入了三个参数,但是add方法只能接受两个参数,因此编译就会失败。但是如果我们改为传入两个参数,编译就可以通过了。但是在运行时,由于我们调用的add方法是传入了三个参数,因此就会产生NoSuchMethodError异常。
1.2 运行时存在方法,但没有被调用
在Java程序中,如果一个类已经被编译成.class文件,但是在运行时没有被使用,JVM就不会加载这个类,也就不会调用这个类中的方法。但是,如果后来又需要使用这个类中的方法,而这个方法已经被修改过了,就会产生NoSuchMethodError异常。
例如,以下是两个版本的类:
public class Test {
public static void main(String[] args) {
A a = new A();
a.sayHello();
}
}
class A {
public void sayHello() {
System.out.println("Hello, World!");
}
}
另一个版本:
public class Test {
public static void main(String[] args) {
A a = new A();
a.sayHello("Java");
}
}
class A {
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
}
如果我们先运行第一个版本的类,然后再运行第二个版本的类,就会产生NoSuchMethodError异常,因为第二个版本的类中的sayHello方法已经发生了变化,但是JVM并没有加载这个新版本的类,而是加载了旧版本的类,因此在调用新版本的sayHello方法时就会产生NoSuchMethodError异常。
二、根本原因
2.1 核心问题
运行时抛出NoSuchMethodError错误的根本原因就是:应用程序直接或间接依赖了同一个类的多个版本,并且在运行时执行了缺少方法的版本。如下图所示:

因此,核心问题就转化为:同一类为什么会有多个版本?哪个版本的类最终会被执行?
2.2 为什么同一个Class会出现多个版本?
导致Java Class出现多版本的原因,可以归纳为以下几类:
- JDK版本不一致。常见于编译打包环境使用高版本
JDK开发与打包,而实际运行环境的JDK版本较低。例如,本地项目环境JDK版本为1.7,调用Character.isAlphabetic()方法判断当前字符是否为字母;而线上环境JDK版本为1.6,在运行期间就会抛出NoSuchMethodError错误。 - SNAPSHOT版本不一致。常见于本地更新
SNAPSHOT版本后,没有执行mvn clean deploy部署,导致线上环境运行时仍然引用了旧版本的SNAPSHOT包。 - Maven依赖生命周期为provided。常见于本地依赖的某组件生命周期为
provided,所声明版本仅用于本地编译打包,而线上运行时会通过其他依赖关系加载Jar包。 - 同一个Jar包出现了多个版本。常见于
Maven依赖未显式指定版本号,导致间接依赖版本冲突,很容易引入低版本的Jar包。 - 同一个Class出现在不同的Jar包中。该问题常见于代码拷贝场景,比如基于开源版本定制了一些功能,使用了新的
Maven坐标打包发布,此时Maven仲裁机制失效(非常隐蔽,难以排查)。由于JVM类加载器对于同一个类只会加载一次,最终加载的类实现受到Jar包依赖的路径、类声明的先后顺序或文件加载顺序等因素的影响,很可能出现不同机器加载的类实现不一致。
2.3 哪个版本的Class最终会被执行?
影响Class最终是否被执行的关键因素有两个:Maven依赖仲裁机制和JVM类加载机制,如下图所示:

首先,Maven依赖仲裁机制 决定了打包的优先级,仲裁优先级“从高到低”如下所述:
- 优先按照依赖管理[dependencyManagement]元素中指定的版本进行仲裁;
- 若无版本声明,则按照“短路径优先”原则(Maven2.0)进行仲裁,即选择依赖树中路径最短的版本;
- 若路径长度一致,则按照“第一声明优先”原则进行仲裁,即选择
POM中最先声明的版本。
合理使用Maven依赖仲裁机制可以便捷的管理Jar包版本,而不合理的使用将导致多版本Jar冲突。
其次,JVM 类加载机制 决定了Class被加载到JVM的优先级,如果同一个类出现在多个Jar包中,那么在双亲委派类加载机制下,加载该Jar包的类加载器层级越高,该Jar包越先被加载,它所包含的Class越先被执行,如上图所示:
- 启动类加载器(Bootstrap ClassLoader)优先级最高,主要加载JVM运行时核心类,如
java.util、java.io等,这些类主要位于$JAVA_HOME/lib/rt.jar文件中。 - 扩展类加载器(Extention ClassLoader)优先级次之,主要加载JVM扩展类,如
swing组件、xml解析器等,这些类主要位于$JAVA_HOME/lib/ext/目录下的Jar包中。 - 应用类加载器(Application ClassLoader),又称系统类加载器,优先级再次之,它会加载
Classpath环境变量里定义的路径中的Jar包和目录,通常我们自己编写的代码或依赖的第三方Jar包都是由它来加载。
除了上述两种原因外,在同一个ClassLoader下,如果存在一个Class出现在不同的Jar包中,那么文件系统的文件加载顺序也可能会影响最终的加载结果。因此,应该尽量保证开发/测试/生产系统环境一致性。
三、解决
虽然抛出NoSuchMethodError错误的原因多种多样,但本质上是由于编译时类路径与运行时类路径不一致。因此,通用的定位思路可以归纳为以下3步:
-
定位异常
Class的全限定类名与调用方,通常可以在应用日志抛出的异常堆栈中获取。如下图所示:Exception in thread "main" java.lang.NoSuchMethodError: com.xxx.AsyncAppender.append(Ljava/lang/String;)Ljava/lang/String; at com.xxx.ProvokeNoSuchMethodError.main(ProvokeNoSuchMethodError:7) at …… -
定位异常
Class的来源,可以通过Arthas等在线诊断工具反编译,如jad com.xxx.AsyncAppender,获取该类运行时的源码、ClassLoader、Jar包位置等信息。
如果应用程序启动失败,或者无法进行在线诊断,可以考虑添加JVM启动参数
-verbose:class或-XX:+TraceClassLoading,在日志中将输出每个类的加载信息,比如来自哪个Jar包。 -
根据
ClassLoader和Jar包全路径名等信息,判断是类加载、Maven仲裁或其他原因,并对应的加以解决。如果是同一个
Jar包的多版本问题,可以在Maven<dependencyManagement>标签中指定实际需要的版本,或者移除间接依赖中的低版本(提示:执行mvn dependency:tree命令,可以查看Maven依赖拓扑关系)。如果是同一个
Class出现在不同的Jar包问题,若可以排除,就用<excludes>排除该依赖;如不能排除,则考虑升级或替换为其他Jar包,或者考虑使用ClassLoader隔离技术,可参考《如果jar包冲突不可避免,如何实现jar包隔离?》。
四、其他Jar包冲突问题
本文介绍的Jar包冲突解决方法,除了解决java.lang.NoSuchMethodError以外,对其他相似问题也具备一定的参考价值。
例如java.lang.ClassNotFoundException,即加载不到指定类,通常是Maven仲裁选错了版本,如本地开发阶段调用了1.2.0版本,而打包时采用了1.0.0版本的Jar包。同理,java.lang.NoClassDefFoundError和java.lang.LinkageError也可以基于上述思路进行排查。
此外,如果类和方法名都保持不变,但是内部实现有变化,在多版本冲突场景下,不会抛出异常,但程序行为跟预期不一致,此时,也可以基于上述思路进行排查诊断。

浙公网安备 33010602011771号