系统稳定性—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依赖仲裁机制 决定了打包的优先级,仲裁优先级“从高到低”如下所述:

  1. 优先按照依赖管理[dependencyManagement]元素中指定的版本进行仲裁;
  2. 若无版本声明,则按照“短路径优先”原则(Maven2.0)进行仲裁,即选择依赖树中路径最短的版本;
  3. 若路径长度一致,则按照“第一声明优先”原则进行仲裁,即选择POM中最先声明的版本。

合理使用Maven依赖仲裁机制可以便捷的管理Jar包版本,而不合理的使用将导致多版本Jar冲突。

其次,JVM 类加载机制 决定了Class被加载到JVM的优先级,如果同一个类出现在多个Jar包中,那么在双亲委派类加载机制下,加载该Jar包的类加载器层级越高,该Jar包越先被加载,它所包含的Class越先被执行,如上图所示:

  1. 启动类加载器(Bootstrap ClassLoader)优先级最高,主要加载JVM运行时核心类,如java.utiljava.io等,这些类主要位于$JAVA_HOME/lib/rt.jar文件中。
  2. 扩展类加载器(Extention ClassLoader)优先级次之,主要加载JVM扩展类,如swing组件xml解析器等,这些类主要位于$JAVA_HOME/lib/ext/目录下的Jar包中。
  3. 应用类加载器(Application ClassLoader),又称系统类加载器,优先级再次之,它会加载Classpath环境变量里定义的路径中的Jar包和目录,通常我们自己编写的代码或依赖的第三方Jar包都是由它来加载。

除了上述两种原因外,在同一个ClassLoader下,如果存在一个Class出现在不同的Jar包中,那么文件系统的文件加载顺序也可能会影响最终的加载结果。因此,应该尽量保证开发/测试/生产系统环境一致性。

三、解决

虽然抛出NoSuchMethodError错误的原因多种多样,但本质上是由于编译时类路径与运行时类路径不一致。因此,通用的定位思路可以归纳为以下3步:

  1. 定位异常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 ……
    
  2. 定位异常Class的来源,可以通过Arthas等在线诊断工具反编译,如jad com.xxx.AsyncAppender,获取该类运行时的源码、ClassLoaderJar包位置等信息。

    如果应用程序启动失败,或者无法进行在线诊断,可以考虑添加JVM启动参数-verbose:class-XX:+TraceClassLoading,在日志中将输出每个类的加载信息,比如来自哪个Jar包。

  3. 根据ClassLoaderJar包全路径名等信息,判断是类加载、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.NoClassDefFoundErrorjava.lang.LinkageError也可以基于上述思路进行排查。

此外,如果类和方法名都保持不变,但是内部实现有变化,在多版本冲突场景下,不会抛出异常,但程序行为跟预期不一致,此时,也可以基于上述思路进行排查诊断。

posted @ 2025-05-16 21:07  夏尔_717  阅读(203)  评论(0)    收藏  举报