上线前一个小时,dubbo这个问题可把我折腾惨了

前因

那是一个月黑风高的夜晚,无论有没有圆圆的月亮,都没法解救要加班的我。这就是苦涩的人生啊!java

那天正好是春节回家的日子,定了晚上的票,而后仍是上线的日子。git

测试在作回归测试的时候,发现一个老功能报错了,什么鬼,都没改过那块代码怎么会出问题?案件疑点重重呀。。。github

为了可以早点上线,早点回家,因此这个Bug就显得十万火急了,由于就这一个问题,其余都没问题,解决好了就能够上线了,因而开启了破案之路。apache

第一步:找到错误信息

机智的我在第一时间打开了Cat查看具体的错误,因为当时并无想到去写一篇文章出来,错误信息也就没有截图,后面经过模拟的操做,获得了相似的同样的错误信息以下:api

图片

竟然是类转换错误,点进去查看详细的错误信息,以下图:bash

图片

真正有价值的错误信息以下:微信

dubbo version: 2.7.3, current host: 192.168.8.224 java.lang.ClassCastException: java.util.HashMap cannot be cast to com.cxytiandi.kittycloud.user.api.request.Address
复制代码

第二步:排查报错的代码

公司代码不方便透露,下面都是模拟的代码:测试

public ResponseData<String> login(UserLoginRequest loginRequest) {
    loginRequest.getAddress().stream().map(a -> a.getStatus()).collect(Collectors.toList());
    return Response.ok("xxxxxxxxx");
}
复制代码

问题就出在了map这里,从loginRequest参数中获取address是一个Listui

,Address中有status字段,若是是正常的对象没有问题,错误告诉咱们是HashMap不能转换成Address类,也就是说参数中的Address变成了HashMap致使的错误。

参数代码:spa

@Data
public class UserLoginRequest implements Serializable {
    private String username;
    private String pass;
    private List<Address> address;
}
@Data
@AllArgsConstructor
public class Address implements Serializable {
    private int status;
}
复制代码

第三步:本地复现错误

找到错误后,立刻本地启动相关的两个服务,咱们分别叫A和B吧,现象是A调用B的某个RPC接口报错。

本地启动后立刻复现了错误,在报错的地方打断点看参数是否变成了HashMap,果不其然,以下图:

图片

到这里感受有点懵,参数中明明是具体的对象类型,怎么忽然就变成了HashMap,匪夷所思。

而后想着是否是在上层什么地方出问题了,继续查看报错的上层代码,没有发现异常。而后决定在PRC的入口处打个断点看看是否是参数一过来就出问题了,最后通过验证确实如此,也就排除了B服务中对参数作了转换。

接着再看下Dubbo内部的参数解码,

org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)。也就是请求到达B以后解码出来的已是HashMap了,那么问题确定是调用方传输的参数有问题。

图片

第四步:排查调用方代码

在调用方这边发起请求前,查看了参数对象,发现这个时候参数已经出问题了,字段类型发生了变化,因此问题就出在这里,都是老代码,应该都没改过,而是事实却被改了,经过Idea的Annotate快速的查看了当前方法中有被修改的记录,找到了修改的代码,下面经过模拟的方式贴出有问题的代码,以下:

@Reference(version = DubboConstant.VERSION_V100, group = DubboConstant.DEFAULT_GROUP)
private UserRemoteService userRemoteService;
public void test() {
    UserLoginRequest request = new UserLoginRequest();
    request.setUsername("yjh");
    request.setPass("123456");
    List<Address> address = new ArrayList<>();
    address.add(new Address(1));
    request.setAddress(address);
    UserLoginRequest2 request2 = new UserLoginRequest2();
    request2.setUsername("yjh2");
    request2.setPass("1234562");
    List<Address2> address2 = new ArrayList<>();
    address2.add(new Address2(StatusEnum.INVALID));
    request2.setAddress(address2);
    
    BeanUtils.copyProperties(request2, request);
    
    userRemoteService.login(request);
}
复制代码

出问题的就是BeanUtils.copyProperties(request2, request); 这行代码,将一个对象复制到另外一个对象,两个对象的属性都同样,惟一不同的是Address中的status是int类型,Address2中的status是Enum,复制过去就出问题了。

图片

这种状况也只在Dubbo的RPC请求出问题,若是是Http请求,基本类型变成了枚举,直接就报错了,没法转换。

图片

第五步:BeanUtils问题排查

归根到底仍是copy的问题,我作了个小实验,若是是Address2 copy到Address 是不会出问题的,只有嵌套的对象才会出问题。

特地看了下copy的代码,若是是Address2 copy到Address,那么就是status到status,在copy以前会进行判断Address的setStatus的第一个参数类型和Address2的getStatus的返回值是否相同,若是相同才会进行赋值操做,不一样就不会,若是是单个对象在这里就会直接过滤掉了,一个是int一个是Enum。

图片

嵌套对象之因此能够那是由于address的参数和返回类型都是List,没有去判断嵌套类里面的,是整个集合直接复制赋值的,下图是目标方法:

图片

value是新的集合对象,invoke后整个address就变了。

图片

第六步:Dubbo解码问题排查

前面分析中,调用以前经过BeanUtils复制,只是将枚举赋值给了基本类型,若是Dubbo在接收到参数进行解码时可以识别出类型不一致,这样就直接会报错了,然而并无,特地调试了下Dubbo解码的代码,默认是Hessian的解码,怀疑跟Hessian有关,因而我把序列化改为了FastJson,在解码参数的时候就直接报错了,不能转换成int类型。而Hessian在映射不了的时候就直接变成HashMap了,这才有了咱们前面的错误。

图片

结局

找到缘由后解决就是分分钟的事了,经过这个问题仍是说明了加任何的代码都有风险。剩下的就是开发的锅了,加了代码没有自测,好在有测试把关,不然就凉凉了。

感兴趣的能够关注下个人微信公众号 猿天地,更多技术文章第一时间阅读。个人GitHub也有一些开源的代码 github.com/yinjihuan

相关文章
相关标签/搜索