代码重构后引起的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
2
3

异常提示信息很明确,没有找到方法org.example.utils.LogUtil.formatStr(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;。Java中方法有签名,这里显示的是字节码层面的方法签名,表示LogUtil中有个formatStr方法,参数包括3个String,返回一个String。具体可以参考Java字节码中的描述。

org.example.utils.LogUtil.formatStr(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

1
2
3

**字节码**从common-lib1(以下简称lib1)项目源代码中找到方法定义如下

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
2
3

- 注意到方法第3个参数是String...,Java中的可变参数的表示方法。

String…

1
2
3

common-lib2(以下简称lib2)项目中调用方法

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
2
3
4
5

从Java语法看lib2调用lib1中formatStr应该没有问题,因为可变参数支持这种调用方式。

那为啥我们这里有java.lang.NoSuchMethodError呢?

java.lang.NoSuchMethodError

1
2
3
4
5
6
7

尝试在lib1中加入下面的方法,重新编译lib1使用lib1新包后发现可以解决问题。

- 注意formatStr方法第3个参数是String

**formatStr**```
public String formatStr(String template, String title, String content) {         return String.format(template, title, content);    }
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
2
3

**替换****新增**解决方法也很简单,就是把去掉的public String formatStr(String template, String title, String content)方法还原就好了。

public String formatStr(String template, String title, String content)

1
2
3
4
5

# 三、思考

整个问题不复杂,代码重构方法中,公共包希望通过API兼容的新方法替换老方法,造成调用方找不到老方法报java.lang.NoSuchMethodError异常。

java.lang.NoSuchMethodError

1
2
3
4
5
6

比较有欺骗性的地方有2个:

1. 因为调用API兼容,觉得可以调用方不动,直接升级公共包就可以
2. 上面提到的两个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;并不准确。定位问题时让人困惑,这个方法不是存在吗为啥找不到?

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
2
3

那么这两个方法的字节码真的一样吗?反编译lib1中Java类org.example.utils.LogUtil中的2个方法,我们可以观察到:

org.example.utils.LogUtil

1
2
3
4

1. 两个方法签名都是一样的descriptor: (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/String;
2. 方法public String formatStr(String template, String title, String... content)字节码中flags中增加了ACC_VARARGS。这个用于说明变参。遗憾的是这个并没有在异常提示信息中出现。

descriptor: (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/String;

1

public String formatStr(String template, String title, String… content)

1
2
3

**flags****ACC_VARARGS**```
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
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