现象
近日在工作中一个小伙伴遇到了一个这样的问题:在生产环境中,使用某个服务时,调用某个服务的方法出现了java.lang.NoSuchMethodError,而该服务另外一个方法却是正常的!!! 具体代码如下:
package cn.bigtiger02;
import cn.bigtiger02.api.ApiResult;
import cn.bigtiger02.service.ApiService;
import org.junit.Assert;
import org.junit.Test;
public class DemoTest {
@Test
public void test1(){
ApiResult result = ApiService.test1();
Assert.assertTrue(result.getFlag());
}
@Test
public void test2(){
ApiResult result = ApiService.test2();
Assert.assertTrue(result.getFlag());
}
}
运行结果如下:
- test1 运行失败,抛出NoSuchMethodError
- test2 运行成功

package cn.bigtiger02.service;
import cn.bigtiger02.api.ApiResult;
public class ApiService {
public static ApiResult test1(){
ApiResult result = new ApiResult();
result.setFlag(true);
result.setMessage("调用成功");
return result;
}
public static ApiResult test2(){
return ApiResult.success("调用成功");
}
}
ApiResult 的代码如下:
package cn.bigtiger02.api;
public class ApiResult {
private boolean flag;
private String message;
public static ApiResult success(String message){
ApiResult result = new ApiResult();
result.setFlag(true);
result.setMessage(message);
return result;
}
public static ApiResult failure(String message){
ApiResult result = new ApiResult();
result.setFlag(false);
result.setMessage(message);
return result;
}
public boolean getFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
经过跟踪分析,竟然是result.setFlag(true)
,抛出了NoSuchMethodError
异常!!!

ApiResult.success()
木有任何问题而使用result.setFlag(true)
却会抛出异常???ApiResult.success()
内部也明明是使用的是result.setFlag(true)
呀。
但是异常不会骗人,java.lang.NoSuchMethodError
明确表明:ApiResult.setFlag(Boolean)该方法不存在。
排查
带着一头雾水,我们到服务器上反编译了ApiService.class
,反编译的结果如下:

true
转变为了Boolean
对象,但反编译服务器上的ApiResult.class
的setFlag(boolean)
的入参却明明是基本类型。

经过一番排查,前一段时间一个小伙伴把ApiResult
中flag
字段的类型由Boolean
对象调整成了boolean
类型。
慢着,如果是由于调整代码导致的问题,我们的各个测试环境应该马上会炸毛的呀? 但我们的各条测试环境却一切正常!!!

service-1.2.jar
的紧急补丁到该生产环境。问题很有可能就出在了这里!!!
服务器上各个jar版本关系如下:

gradle
打jar,制作了service-1.2.jar
补丁!!!
我们马上登录到小伙伴的IDE,通过导航,进入反编译后的ApiResult.class
,方法入参竟然是基本类型setFlag(boolean)
!!! 这就纳闷了。为何制作的新jar中,ApiResult.setFlag()
需要将true
装箱成Boolean
对象?

怀着疑惑的心情,我们搜索了一下ApiResult.class
,竟然有两个api.jar
包!!!其中一个是老版本的api.jar
!!!

build.gradle
,依赖的是老的jar包。

具体原因已经查明。该小伙伴为了调试其他bug,在同一工作空间引入了其他工程,带入了最新的api.jar
包,因此看起来编译时使用的是最新的api.jar包。打补丁时通过gradle
的depencies
依赖的却是老的jar包!!!因此打包的时候编译器会自动将boolean装箱成Boolean对象!!!
总结
- Java 是静态编译语言,在编译的时候会通过引用关系确定具体方法的参数类型。如若在打包的时候引用了错误的第三方版本,则在使用的时候会抛出相关异常
- 对于 Java 自动装箱机制,有利有弊。在某些情况下会引发很隐蔽的问题,导致很难排查。在类设计时就需要充分考虑自动装箱带来的影响,以免带来额外的转换开销及隐晦的bug
- 我们所看到的,不一定就是全部的真相。