一次NoSuchMethodError排查过程

背景

首先说明一下项目结构,是一个多模块的maven项目,其中有项目A,以及公共模块C,其中公共模块C依赖了一个外部SDK。

项目A:pom.xml

xml 复制代码
<dependency>
	<groupId>xxx.xxx</groupId>
	<artifactId>module-C</artifactId>
	<version>1.0.0</version>
</dependency>

公共模块C:pom.xml

xml 复制代码
<dependency>
	<groupId>xxx.sdk</groupId>
	<artifactId>sdk</artifactId>
	<version>1.0.0</version>
</dependency>

然后某次变更,升级了SDK的一个大版本,但是只有项目A依赖此版本,因此没有更新公共模块,而是在项目A中添加了对应的SDK依赖,并将在公共模块中exclude掉了,如下:

项目A:pom.xml

xml 复制代码
<dependency>
	<groupId>xxx.xxx</groupId>
	<artifactId>module-C</artifactId>
	<version>1.0.0</version>
	<exclusion>
        <groupId>xxx.sdk</groupId>
        <artifactId>sdk</artifactId>
    </exclusion>
</dependency>
<dependency>
	<groupId>xxx.sdk</groupId>
	<artifactId>sdk</artifactId>
	<version>2.0.0</version>
</dependency>

这一改就出事了,项目A启动时,在执行到sdk的某个方法时,直接报错NoSuchMethodError

排查过程

首先maven打包正常,那么编译的时候是正常的,方法肯定存在。但是在运行时没有找到对应的方法,说明运行时和编译时方法签名不一致。

排查依赖冲突

一般NoSuchMethodError异常或ClassNotFound这种情况,大部分情况下是版本冲突导致的,编译时用的一个版本,运行时用一另一个版本,导致异常,那需要先排查一下运行的依赖,是否有多个版本存在

grep -R "xxx.xxx.Class"

在lib目录下通过grep查询异常类在哪些jar中存在,-R会递归查找所有子目录

但是奇怪的是,只有一个jar包含了这个类,也是我们预期的新版本SDK,只有一个版本为什么会冲突?再尝试通过arthas查看内存中的类

arthas的使用方式可查看官方文档:arthas.aliyun.com/doc/command...

Arthas排查加载类

进入arthas控制台后,通过jad命令对加载的类进行反编译
jad xxx.xxx.Class

查看反编译对应的类,看起来好像也没有什么问题,方法是存在的。

再通过sm命令看看方法签名,sm可以列出指定类的所有方法签名
sm xxx.xxx.Class

这里就发现问题了,内存中的类方法签名和我们报错中的方法签名不一样。报错的返回值一个是原始数据类型bool(Z),而加载的类的返回值却是一个是Boolean对象(Ljava/lang/Boolean;)。

javap查看字节码

为什么会不一样?由于方法签名是在编译的时候确定的,我们再去查看一下调用类的字节码,看看是不是符合预期。通过JDK自带的工具javap可以查看指定类的字节码:
javap -verbose A.class

可以看到,字节码里面的方法签名确实是错的,再去查看SDK的两个版本,发现旧版本的方法返回值确实是使用的基础数据类型,而新版本改为了Boolean对象。 但是为什么?我们明明exclude掉了旧的版本,最终打包出来的应用里面,通过grep也确认了只有一个新版本。那为什么编译的时候不符合我们的预期?

异常原因

既然是编译的时候不符合预期,那再去梳理maven的打包流程,发现了问题。整个构建流程,由于模块A依赖了公共模块C,那打包时肯定先构建模块C,而此时它依赖的SDK-1.0.0,这时编译的方法签名是旧的,然后在构建模块A时,由于SDK-1.0.0被exclude掉了,那么打包时会将此依赖排除,最终打包到项目中的是我们配置的2.0.0。

解决

问题找到了就很好解决,只能直接更新公共模块了。

其它

其实在排查过程中还遇到一些其它的问题,最开始的时候,有尝试在arthas中去watch异常点的上层方法,使用一样的参数去调用对应的异常方法,发现并不会报异常,但是执行到源代码中对应的代码时就会抛出异常。

watch xxx.xxx.Class method `{target.manager.do(params0, params1)}` watch时可以通过target拿到原始类,然后执行对应的成员变量的方法

还有查看方法签名时,一开始没有关注返回值,一直在对比方法参数,死活看不出异常,后面才发现签名中的返回值不一样。

最后,这个问题其实比较简单,但是排查方式比较通用,所以分享一下排查过程,在遇到类似的问题时,都可以使用同样的方式来处理。

相关推荐
程序员爱钓鱼4 分钟前
Rust 切片 Slice 详解:安全访问连续数据
前端·后端·rust
要开心吖ZSH1 小时前
一文搞懂:JDK 21 虚拟线程 vs N种异步方案,到底该怎么选?
java·数据库·jdk·虚拟线程
guodingdingh6 小时前
软件开发工作问题总结0718
java·开发语言·数据库
咖啡八杯8 小时前
GoF设计模式——解释器模式
java·后端·spring·设计模式
优橙教育8 小时前
5G网优培训 vs Java开发:转行选哪个?
java·开发语言·5g
掘金码甲哥8 小时前
这块终端神器, 必须吹爆!
后端
糖果店的幽灵8 小时前
【DeepAgents 从入门到精通】Context Management 上下文管理
java·人工智能·后端·spring·中间件·langgraph·deepagents
Csvn9 小时前
📊 SQL 入门 Day 8:集合操作 — 用 SQL 做数学里的"并交差"
后端·sql
腻害兔10 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:字典、短信、邮件、通知——后台系统的“基础设施四件套“!
java·前端·vue.js·产品经理·ai编程
碎光拾影10 小时前
ARM交叉工具链各工具作用及IMX6ULL平台LED+蜂鸣器裸机程序实现
java·开发语言·数据库