最近遇到了一次诡异的 NoSuchMethodError
错误,通常情况下遇到这个报错,大概率是类加载冲突导致的,然而这次的情况却不大一样。
背景 按照以往的经验,如果是类加载冲突导致的出错,那么实际加载的类与预期的会不一致。
要确认最终加载了哪个类,可以在 jvm 启动参数里加上 -verbose:class
,这样在程序启动后会打印出类加载日志。如下所示是我截取了一段日志。
1 2 3 4 5 6 [Loaded com.gorden5566.Test from file:/home/gorden5566/github/nosuchmethod/client/target/classes/] [Loaded java.lang.Void from /home/gorden5566/Application/jdk1.7.0_79/jre/lib/rt.jar] [Loaded com.gorden5566.ShowUtil from file:/home/gorden5566/github/nosuchmethod/sdk/target/classes/] [Loaded com.gorden5566.Shape from file:/home/gorden5566/github/nosuchmethod/common/target/classes/] [Loaded com.gorden5566.Circle from file:/home/gorden5566/github/nosuchmethod/common/target/classes/] [Loaded com.gorden5566.ShapeUtil from file:/home/gorden5566/github/nosuchmethod/common/target/classes/]
从类加载日志中可以看到加载的类名以及所在的路径。如果类是在 jar 包中,则打印出 jar 包的完整路径名。而对于当前项目中的类,则是打印出编译出的 class 文件所在的路径。
通过类加载日志可以定位到有问题的类是从哪个地方加载的。如果是从一个非预期的 jar 包中加载的,那么说明存在 jar 包冲突,也就是说两个 jar 包中存在命名完全相同的类(类名一样并且包路径也一样)。解决了 jar 包冲突,也就解决了类冲突的问题。
重现报错 然而这次的情况并非是类加载冲突。通过猜测和排除法最终确定是方法签名变更导致,为了方便描述问题,我建了一个项目 nosuchmethod ,项目结构如下
1 2 3 4 5 6 7 8 9 10 ├── client │ ├── pom.xml │ └── src ├── common │ ├── pom.xml │ └── src ├── pom.xml └── sdk ├── pom.xml └── src
项目中共包含 3 个模块:common、sdk 和 client。其中 common 是用于提供公共工具的类库,sdk 用于对外提供特定功能,它依赖于 common 模块,client 表示业务功能,它依赖于 common 和 sdk 模块
1 2 3 sdk -> common client -> sdk client -> common
common模块 common 模块提供了一个 ShapeUtil 类,它提供了一个 show 方法,方法内部会调用 shape 实例的 show 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class ShapeUtil { public static void show (Shape shape) { if (shape != null ) { shape.show(); } } } public interface Shape { void show () ; } public class Circle implements Shape { @Override public void show () { System.out.println("I am a circle" ); } }
在 init 分支中,ShapeUtil 的 show 方法的入参类型为 Shape
,打包出来的版本号为 1.0-low-RELEASE
在 update 分支中,ShapeUtil 的 show 方法的入参类型为 Circle
,打包出来的版本号为 1.0-high-RELEASE
sdk 模块 sdk 中仅有一个类 ShowUtil,它的 show 方法调用了 common 中的 ShapeUtil 的 show 方法,注意入参是 Circle 类型的实例
1 2 3 4 5 6 7 8 9 public class ShowUtil { public static void show () { ShapeUtil.show(new Circle()); } }
打包出来的版本为 1.0-low-RELEASE
client 模块 client 模块只有一个类 Test,在它的 main 方法中调用了 sdk 中的 ShowUtil 的 show 方法
1 2 3 4 5 6 7 8 9 public class Test { public static void main (String[] args) { ShowUtil.show(); } }
在 init 分支中,依赖的 common 模块和 sdk 模块的版本均为 1.0-low-RELEASE
在 update 分支中,依赖的 common 模块版本升级为 1.0-high-RELEASE
,但是 sdk 的版本仍保持为 1.0-low-RELEASE
1 2 3 4 5 6 7 8 9 10 11 <dependency> <groupId>com.gorden5566</groupId> <artifactId>common</artifactId> <version>1.0-high-RELEASE</version> </dependency> <dependency> <groupId>com.gorden5566</groupId> <artifactId>sdk</artifactId> <version>1.0-low-RELEASE</version> </dependency>
复现问题步骤 要重现前述问题,可按如下步骤操作:
切换到该项目的 init
分支,执行 mvn clean install
命令,编译项目并将 common
、sdk
的 1.0-low-RELEASE
版本安装到本地仓库
切换到 update
分支,执行 mvn clean install
命令再次编译项目
找到 client
模块下的 com.gorden5566.Test
类,运行其 main
方法,不出意外的话你将看到如下报错
1 2 3 Exception in thread "main" java.lang.NoSuchMethodError: com.gorden5566.ShapeUtil.show(Lcom/gorden5566/Shape;)V at com.gorden5566.ShowUtil.show(ShowUtil.java:9 ) at com.gorden5566.Test.main(Test.java:9 )
问题原因 对于一个问题,如果能够稳定复现,那么离找到问题原因也不远了。
在上面的报错中,可以看到是找不到 ShapeUtil 的一个 show 方法,该方法的签名为 show(Lcom/gorden5566/Shape;)V
,从中可以看到它的入参类型为 com.gorden5566.Shape
,对应的是使用 init 分支打包出来的 1.0-low-RELEASE
版的 common 模块。
回顾下 update 分支中 client 依赖的版本号
1 2 3 4 5 6 7 8 9 10 11 <dependency> <groupId>com.gorden5566</groupId> <artifactId>common</artifactId> <version>1.0-high-RELEASE</version> </dependency> <dependency> <groupId>com.gorden5566</groupId> <artifactId>sdk</artifactId> <version>1.0-low-RELEASE</version> </dependency>
此时依赖的 sdk 是 1.0-low-RELEASE
版,它在打包时 ShapeUtil 提供的 show 方法是 public static void show(Shape shape) ;
然而 common 包已经升级到 1.0-high-RELEASE
,ShapeUtil 的 show 方法签名已经变更 public static void show(Circle shape) ;
注意这是两个不同的方法,可以重载但不是重写。
1 2 public static void show (Shape shape) ;public static void show (Circle shape) ;
我们有理由相信,正是打包时记录的链接信息不同,进而导致出现 java.lang.NoSuchMethodError
错误。
为进一步验证,我们可以通过 javap
命令查看 1.0-low-RELEASE
版 sdk 的链接信息和 1.0-high-RELEASE
版的 common 包提供的方法信息,确认是否真是这样。
解压 common-1.0-high-RELEASE.jar
,执行 javap ShapeUtil
查看 ShapeUtil.class 类文件
1 2 3 4 5 6 警告: 二进制文件ShapeUtil包含com.gorden5566.ShapeUtil Compiled from "ShapeUtil.java" public class com .gorden5566 .ShapeUtil { public com.gorden5566.ShapeUtil(); public static void show (com.gorden5566.Circle) ; }
可以看到方法提供的 show 方法为 public static void show(com.gorden5566.Circle);
解压 sdk-1.0-low-RELEASE.jar
,执行 javap -verbose ShowUtil
,输出结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 警告: 二进制文件ShowUtil包含com.gorden5566.ShowUtil Classfile /home/gorden5566/test/com/gorden5566/ShowUtil.class Last modified 2021-4-7; size 439 bytes MD5 checksum a650d8f8addd854ec094d908c0ad2444 Compiled from "ShowUtil.java" public class com.gorden5566.ShowUtil minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#17 // java/lang/Object."<init>":()V #2 = Class #18 // com/gorden5566/Circle #3 = Methodref #2.#17 // com/gorden5566/Circle."<init>":()V #4 = Methodref #19.#20 // com/gorden5566/ShapeUtil.show:(Lcom/gorden5566/Shape;)V #5 = Class #21 // com/gorden5566/ShowUtil #6 = Class #22 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/gorden5566/ShowUtil; #14 = Utf8 show #15 = Utf8 SourceFile #16 = Utf8 ShowUtil.java #17 = NameAndType #7:#8 // "<init>":()V #18 = Utf8 com/gorden5566/Circle #19 = Class #23 // com/gorden5566/ShapeUtil #20 = NameAndType #14:#24 // show:(Lcom/gorden5566/Shape;)V #21 = Utf8 com/gorden5566/ShowUtil #22 = Utf8 java/lang/Object #23 = Utf8 com/gorden5566/ShapeUtil #24 = Utf8 (Lcom/gorden5566/Shape;)V { public com.gorden5566.ShowUtil(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/gorden5566/ShowUtil; public static void show(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=0, args_size=0 0: new #2 // class com/gorden5566/Circle 3: dup 4: invokespecial #3 // Method com/gorden5566/Circle."<init>":()V 7: invokestatic #4 // Method com/gorden5566/ShapeUtil.show:(Lcom/gorden5566/Shape;)V 10: return LineNumberTable: line 9: 0 line 10: 10 } SourceFile: "ShowUtil.java"
注意第 58 行 Method com/gorden5566/ShapeUtil.show:(Lcom/gorden5566/Shape;)V
,它表明 ShowUtil 调用的是 ShapeUtil 的 show 方法签名为 (Lcom/gorden5566/Shape;)V
,其中入参类型为 com.gorden.5566.Shape
,确实与 common-1.0-high-RELEASE.jar
包提供的方法不一致
至此也就定位到了原因
总结 java.lang.NoSuchMethodError
不是 Exception,一般的 try catch 并不能捕获这个错误,如果不慎遇到这个问题,极有可能会造成系统无法正常工作。
对外的 api 或工具类,一旦提供出去,就不要再修改定义,可以新增方法,但不要删除或修改方法,除非确定确实没有问题。
方法签名变更导致的不一致问题,在 IDEA 里查看源码不会有错误提示,这也使得问题的排查变得更难。java 官方提供的 javap 等工具在关键时候还是挺有用的。
如果你有遇到类似问题,希望本文可以对你有所帮助。