一次诡异的NoSuchMethodError问题排查
最近遇到了一次诡异的 NoSuchMethodError
错误,通常情况下遇到这个报错,大概率是类加载冲突导致的,然而这次的情况却不大一样。
背景
按照以往的经验,如果是类加载冲突导致的出错,那么实际加载的类与预期的会不一致。
要确认最终加载了哪个类,可以在 jvm 启动参数里加上 -verbose:class
,这样在程序启动后会打印出类加载日志。如下所示是我截取了一段日志。
1 | [Loaded com.gorden5566.Test from file:/home/gorden5566/github/nosuchmethod/client/target/classes/] |
从类加载日志中可以看到加载的类名以及所在的路径。如果类是在 jar 包中,则打印出 jar 包的完整路径名。而对于当前项目中的类,则是打印出编译出的 class 文件所在的路径。
通过类加载日志可以定位到有问题的类是从哪个地方加载的。如果是从一个非预期的 jar 包中加载的,那么说明存在 jar 包冲突,也就是说两个 jar 包中存在命名完全相同的类(类名一样并且包路径也一样)。解决了 jar 包冲突,也就解决了类冲突的问题。
重现报错
然而这次的情况并非是类加载冲突。通过猜测和排除法最终确定是方法签名变更导致,为了方便描述问题,我建了一个项目 nosuchmethod ,项目结构如下
1 | ├── client |
项目中共包含 3 个模块:common、sdk 和 client。其中 common 是用于提供公共工具的类库,sdk 用于对外提供特定功能,它依赖于 common 模块,client 表示业务功能,它依赖于 common 和 sdk 模块
1 | sdk -> common |
common模块
common 模块提供了一个 ShapeUtil 类,它提供了一个 show 方法,方法内部会调用 shape 实例的 show 方法
1 | /** |
在 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 | /** |
打包出来的版本为 1.0-low-RELEASE
client 模块
client 模块只有一个类 Test,在它的 main 方法中调用了 sdk 中的 ShowUtil 的 show 方法
1 | /** |
在 init 分支中,依赖的 common 模块和 sdk 模块的版本均为 1.0-low-RELEASE
在 update 分支中,依赖的 common 模块版本升级为 1.0-high-RELEASE
,但是 sdk 的版本仍保持为 1.0-low-RELEASE
1 | <dependency> |
复现问题步骤
要重现前述问题,可按如下步骤操作:
切换到该项目的
init
分支,执行mvn clean install
命令,编译项目并将common
、sdk
的1.0-low-RELEASE
版本安装到本地仓库切换到
update
分支,执行mvn clean install
命令再次编译项目找到
client
模块下的com.gorden5566.Test
类,运行其main
方法,不出意外的话你将看到如下报错1
2
3Exception 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 | <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 | public static void show(Shape 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 | 警告: 二进制文件ShapeUtil包含com.gorden5566.ShapeUtil |
可以看到方法提供的 show 方法为 public static void show(com.gorden5566.Circle);
解压 sdk-1.0-low-RELEASE.jar
,执行 javap -verbose ShowUtil
,输出结果如下
1 | 警告: 二进制文件ShowUtil包含com.gorden5566.ShowUtil |
注意第 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 等工具在关键时候还是挺有用的。
如果你有遇到类似问题,希望本文可以对你有所帮助。
- 2017-12-03
本次编译使用的系统是
macOS High Sierra
,版本为10.13.1
。使用的 jdk 是Oracle JDK
,版本为1.7.0_79
。1
2
3java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode) - 2021-01-30
Mac 下编译 netty 报错,提示
Netty/Transport/Native/Unix/Common
模块编译失败,到网上搜索一下,并未发现有人遇到过类似问题,因此做下记录。 - 2020-12-04
Rpc Agent is a framework, with which you can develop an agent server for a RPC framework.