纳尼?obj.setXXX()这么简单的方法居然还会抛异常?

242 阅读3分钟

现象

近日在工作中一个小伙伴遇到了一个这样的问题:在生产环境中,使用某个服务时,调用某个服务的方法出现了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,反编译的结果如下:

java通过自动装箱机制,将true转变为了Boolean对象,但反编译服务器上的ApiResult.classsetFlag(boolean)的入参却明明是基本类型。

经过一番排查,前一段时间一个小伙伴把ApiResultflag字段的类型由Boolean对象调整成了boolean类型。

慢着,如果是由于调整代码导致的问题,我们的各个测试环境应该马上会炸毛的呀? 但我们的各条测试环境却一切正常!!!

继续排查,发现最近一段时间另一个小伙伴最近制作了一个service-1.2.jar的紧急补丁到该生产环境。问题很有可能就出在了这里!!!

服务器上各个jar版本关系如下:

我们上线时是1.1的版本,小伙伴是通过IDE修改代码后,直接通过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包。打补丁时通过gradledepencies依赖的却是老的jar包!!!因此打包的时候编译器会自动将boolean装箱成Boolean对象!!!

总结

  • Java 是静态编译语言,在编译的时候会通过引用关系确定具体方法的参数类型。如若在打包的时候引用了错误的第三方版本,则在使用的时候会抛出相关异常
  • 对于 Java 自动装箱机制,有利有弊。在某些情况下会引发很隐蔽的问题,导致很难排查。在类设计时就需要充分考虑自动装箱带来的影响,以免带来额外的转换开销及隐晦的bug
  • 我们所看到的,不一定就是全部的真相。