代码重构后的java.lang.NoSuchMethodError
一、问题
Java项目最近发布后遇到一个java.lang.NoSuchMethodError异常,定位下来是一个跟公共组件包升级有关的常见问题,这里记录一下踩坑经历。
二、分析
有个业务接入了公共包common-lib2,common-lib2依赖common-lib1。服务编译、构建、服务启动都没有问题,但是访问服务运行时系统异常并提示类似下面日志(脱敏后的模拟服务):
1 | Exception in thread "main" java.lang.NoSuchMethodError: org.example.utils.LogUtil.formatStr(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; |
异常提示信息很明确,没有找到方法org.example.utils.LogUtil.formatStr(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;。Java中方法有签名,这里显示的是字节码层面的方法签名,表示LogUtil中有个formatStr方法,参数包括3个String,返回一个String。具体可以参考Java字节码中的描述。
从common-lib1(以下简称lib1)项目源代码中找到方法定义如下
1 | public String formatStr(String template, String title, String... content) { |
- 注意到方法第3个参数是
String...,Java中的可变参数的表示方法。
common-lib2(以下简称lib2)项目中调用方法
1 | public static void main(String[] args) { |
从Java语法看lib2调用lib1中formatStr应该没有问题,因为可变参数支持这种调用方式。
那为啥我们这里有java.lang.NoSuchMethodError呢?
尝试在lib1中加入下面的方法,重新编译lib1使用lib1新包后发现可以解决问题。
- 注意formatStr方法第3个参数是String
1 | public String formatStr(String template, String title, String content) { |
那问题是怎么产生和解决的呢?从上面看到LogUtil两个formatStr方法是有区别的,虽然从调用方来看,语言API上是兼容的(都可以调用),但是JVM层面是不同的方法。从lib1中代码修改的git记录看,在一次代码重构中,我们把方法public String formatStr(String template, String title, String content)去掉了,增加了public String formatStr(String template, String title, String... content)。重构时使用了替换而不是新增的方式。
解决方法也很简单,就是把去掉的public String formatStr(String template, String title, String content)方法还原就好了。
三、思考
整个问题不复杂,代码重构方法中,公共包希望通过API兼容的新方法替换老方法,造成调用方找不到老方法报java.lang.NoSuchMethodError异常。
比较有欺骗性的地方有2个:
- 因为调用API兼容,觉得可以调用方不动,直接升级公共包就可以
- 上面提到的两个
formatStr方法字节码层面的签名是一样的,异常提示信息Exception in thread "main" java.lang.NoSuchMethodError: org.example.utils.LogUtil.formatStr(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;并不准确。定位问题时让人困惑,这个方法不是存在吗为啥找不到?
那么这两个方法的字节码真的一样吗?反编译lib1中Java类org.example.utils.LogUtil中的2个方法,我们可以观察到:
- 两个方法签名都是一样的
descriptor: (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/String; - 方法
public String formatStr(String template, String title, String... content)字节码中flags中增加了ACC_VARARGS。这个用于说明变参。遗憾的是这个并没有在异常提示信息中出现。
1 | javap -v org.example.utils.LogUtil |
四、展开
Java中下面这2个formatStr方法称为Overload(重载),那么调用方是怎么确定使用哪一个呢?
public String formatStr(String template, String title, String content)
public String formatStr(String template, String title, String... content)
- 首先重载是静态绑定,编译期间就确定了使用哪一个方法;
- 方法选择规则
- 参数数量匹配:优先匹配参数数量完全一致的方法
- 参数类型匹配,编译器会优先选择参数类型完全匹配的方法。对于调用
log.formatStr("title: %s, content: %s", "Hello, World!", "This is a log message.")编译器会优先选择非变参的方法
其实从上面可以看出我们还有另外一个解决问题的方法就是不改动lib1,重新编译构建lib2即可。缺点是所有调用方都需要重新编译,这对于调用方多的场景就不合适了。建议在lib1中修改,调用方都不动。
五、参考
为了方便复现问题,可以参考下面github代码仓库。
1 | # 在lib1项目目录,编译lib1 |