代码重构后引起的java.lang.NoSuchMethodError
代码重构后引起的java.lang.NoSuchMethodError
一、问题
Java项目最近发布后遇到一个java.lang.NoSuchMethodError异常,定位下来是一个跟公共组件包升级有关的常见问题,这里记录一下踩坑经历。
1 | java.lang.NoSuchMethodError |
二、分析
有个业务接入了公共包common-lib2,common-lib2依赖common-lib1。服务编译、构建、服务启动都没有问题,但是访问服务运行时系统异常并提示类似下面日志(脱敏后的模拟服务):
脱敏后的模拟服务```
Exception in thread “main” java.lang.NoSuchMethodError: org.example.utils.LogUtil.formatStr(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; at org.example.Main2.main(Main2.java:8)
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; at org.example.Main2.main(Main2.java:8)
1 |
|
org.example.utils.LogUtil.formatStr(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
1 |
|
public String formatStr(String template, String title, String… content) { if (content.length > 0) { return String.format(template, title, content[0]); } else { return String.format(template, title, content[0], content[1]); } }
1 |
public String formatStr(String template, String title, String… content) { if (content.length > 0) { return String.format(template, title, content[0]); } else { return String.format(template, title, content[0], content[1]); } }
1 |
|
String…
1 |
|
public static void main(String[] args) { LogUtil log = new LogUtil(); String retStr = log.formatStr(“title: %s, content: %s”, “Hello, World!”, “This is a log message.”); System.out.println(retStr); }
1 |
public static void main(String[] args) { LogUtil log = new LogUtil(); String retStr = log.formatStr(“title: %s, content: %s”, “Hello, World!”, “This is a log message.”); System.out.println(retStr); }
1 |
|
java.lang.NoSuchMethodError
1 |
|
1 | public String formatStr(String template, String title, String content) { return String.format(template, title, 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)。重构时使用了替换而不是新增的方式。
LogUtil**formatStr```
public String formatStr(String template, String title, String content)
1 |
public String formatStr(String template, String title, String… content)
1 |
|
public String formatStr(String template, String title, String content)
1 |
|
java.lang.NoSuchMethodError
1 |
|
formatStr
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;
1 |
|
org.example.utils.LogUtil
1 |
|
descriptor: (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/String;
1 |
public String formatStr(String template, String title, String… content)
1 |
|
1 | javap -v org.example.utils.LogUtil...省略 public java.lang.String formatStr(java.lang.String, java.lang.String, java.lang.String...); descriptor: (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/String; flags: ACC_PUBLIC, ACC_VARARGS...省略 public java.lang.String formatStr(java.lang.String, java.lang.String, java.lang.String); descriptor: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; flags: ACC_PUBLIC |
四、展开
Java中下面这2个formatStr方法称为Overload(重载),那么调用方是怎么确定使用哪一个呢?
1 | formatStr |
**Overload(重载)**public String formatStr(String template, String title, String content)
1 | public String formatStr(String template, String title, String content) |
public String formatStr(String template, String title, String… content)
1 | public String formatStr(String template, String title, String... content) |
- 首先重载是静态绑定,编译期间就确定了使用哪一个方法;
- 方法选择规则
- 参数数量匹配:优先匹配参数数量完全一致的方法
- 参数类型匹配,编译器会优先选择参数类型完全匹配的方法。对于调用log.formatStr(“title: %s, content: %s”, “Hello, World!”, “This is a log message.”)编译器会优先选择非变参的方法
重载1. 参数数量匹配:优先匹配参数数量完全一致的方法
2. 参数类型匹配,编译器会优先选择参数类型完全匹配的方法。对于调用log.formatStr(“title: %s, content: %s”, “Hello, World!”, “This is a log message.”)编译器会优先选择非变参的方法
1 | log.formatStr("title: %s, content: %s", "Hello, World!", "This is a log message.") |
非变参其实从上面可以看出我们还有另外一个解决问题的方法就是不改动lib1,重新编译构建lib2即可。缺点是所有调用方都需要重新编译,这对于调用方多的场景就不合适了。建议在lib1中修改,调用方都不动。
五、参考
为了方便复现问题,可以参考下面github代码仓库。
1 | # 在lib1项目目录,编译lib1mvn compile# 在lib1项目目录,安装lib1 jar到本地.m2库mvn install# 在lib2项目目录,命令行执行lib2主方法java -cp ~/.m2/repository/org/example/common-lib1/1.0-SNAPSHOT/common-lib1-1.0-SNAPSHOT.jar:target/classes org.example.Main2 |
1 | # 在lib1项目目录,编译lib1mvn compile# 在lib1项目目录,安装lib1 jar到本地.m2库mvn install# 在lib2项目目录,命令行执行lib2主方法java -cp ~/.m2/repository/org/example/common-lib1/1.0-SNAPSHOT/common-lib1-1.0-SNAPSHOT.jar:target/classes org.example.Main2 |