前因
那是一个月黑风高的夜晚,不管有没有圆圆的月亮,都无法解救要加班的我。这就是苦涩的人生啊!
那天正好是春节回家的日子,定了晚上的票,然后还是上线的日子。
测试在做回归测试的时候,发现一个老功能报错了,什么鬼,都没改过那块代码怎么会出问题?案件疑点重重呀。。。
为了能够早点上线,早点回家,所以这个 Bug 就显得十万火急了,因为就这一个问题,其他都没问题,解决好了就可以上线了,于是开启了破案之路。
第一步:找到错误信息
机智的我在第一时间打开了 Cat 查看具体的错误,由于当时并没有想到去写一篇文章出来,错误信息也就没有截图,后面通过模拟的操作,得到了类似的一样的错误信息如下:
上线前一个小时,dubbo这个问题可把我折腾惨了
Cat错误信息
居然是类转换错误,点进去查看详细的错误信息,如下图:
上线前一个小时,dubbo这个问题可把我折腾惨了_第1张图片
Cat错误详情
真正有价值的错误信息如下:
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 login(UserLoginRequest loginRequest) {
    loginRequest.getAddress().stream().map(a -> a.getStatus()).collect(Collectors.toList());
    return Response.ok("xxxxxxxxx");
}

问题就出在了 map 这里,从 loginRequest 参数中获取 address 是一个 List
,Address 中有 status 字段,如果是正常的对象没有问题,错误告诉我们是 HashMap 不能转换成 Address 类,也就是说参数中的 Address 变成了 HashMap 导致的错误。
参数代码:

@Data
public class UserLoginRequest implements Serializable {
    private String username;
    private String pass;
    private List
address; } @Data @AllArgsConstructor public class Address implements Serializable { private int status; }

第三步:本地复现错误
找到错误后,马上本地启动相关的两个服务,我们分别叫 A 和 B 吧,现象是 A 调用 B 的某个 RPC 接口报错。
本地启动后马上复现了错误,在报错的地方打断点看参数是否变成了 HashMap,果不其然,如下图:
上线前一个小时,dubbo这个问题可把我折腾惨了_第2张图片
参数信息
到这里感觉有点懵,参数中明明是具体的对象类型,怎么突然就变成了 HashMap,匪夷所思。
然后想着是不是在上层什么地方出问题了,继续查看报错的上层代码,没有发现异常。然后决定在 PRC 的入口处打个断点看看是不是参数一过来就出问题了,最后经过验证确实如此,也就排除了 B 服务中对参数做了转换。
接着再看下 Dubbo 内部的参数解码,
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)。也就是请求到达 B 之后解码出来的已经是 HashMap 了,那么问题肯定是调用方传输的参数有问题。
上线前一个小时,dubbo这个问题可把我折腾惨了_第3张图片
Dubbo内部参数查看
第四步:排查调用方代码
在调用方这边发起请求前,查看了参数对象,发现这个时候参数已经出问题了,字段类型发生了变化,所以问题就出在这里,都是老代码,应该都没改过,而是事实却被改了,通过 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 = new ArrayList<>(); address.add(new Address(1)); request.setAddress(address); UserLoginRequest2 request2 = new UserLoginRequest2(); request2.setUsername("yjh2"); request2.setPass("1234562"); List 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这个问题可把我折腾惨了_第4张图片
属性复制
这种情况也只在 Dubbo 的 RPC 请求出问题,如果是 Http 请求,基本类型变成了枚举,直接就报错了,无法转换。
上线前一个小时,dubbo这个问题可把我折腾惨了_第5张图片
Http请求错误
第五步:BeanUtils 问题排查
归根到底还是 copy 的问题,我做了个小实验,如果是 Address2 copy 到 Address 是不会出问题的,只有嵌套的对象才会出问题。
特意看了下 copy 的代码,如果是 Address2 copy 到 Address,那么就是 status 到 status,在 copy 之前会进行判断 Address 的 setStatus 的第一个参数类型和 Address2 的 getStatus 的返回值是否相同,如果相同才会进行赋值操作,不同就不会,如果是单个对象在这里就会直接过滤掉了,一个是 int 一个是 Enum。
上线前一个小时,dubbo这个问题可把我折腾惨了_第6张图片
BeanUtils源码
嵌套对象之所以可以那是因为 address 的参数和返回类型都是 List,没有去判断嵌套类里面的,是整个集合直接复制赋值的,下图是目标方法:
上线前一个小时,dubbo这个问题可把我折腾惨了_第7张图片
BeanUtils源码
value 是新的集合对象,invoke 后整个 address 就变了。
上线前一个小时,dubbo这个问题可把我折腾惨了_第8张图片
嵌套对象复制后
第六步:Dubbo 解码问题排查
前面分析中,调用之前通过 BeanUtils 复制,只是将枚举赋值给了基本类型,如果 Dubbo 在接收到参数进行解码时能够识别出类型不一致,这样就直接会报错了,然而并没有,特意调试了下 Dubbo 解码的代码,默认是 Hessian 的解码,怀疑跟 Hessian 有关,于是我把序列化改成了 FastJson,在解码参数的时候就直接报错了,不能转换成 int 类型。而 Hessian 在映射不了的时候就直接变成 HashMap 了,这才有了我们前面的错误。
上线前一个小时,dubbo这个问题可把我折腾惨了_第9张图片
FastJson解码失败
结局
找到原因后解决就是分分钟的事了,通过这个问题还是说明了加任何的代码都有风险。剩下的就是开发的锅了,加了代码没有自测,好在有测试把关,否则就凉凉了。
热文推荐
Spring Boot 与微服务从0到1的实践

我在外包公司做增删改查有前途么?

那天晚上和@FeignClient注解的深度交流

上线前一个小时,dubbo这个问题可把我折腾惨了_第10张图片

上线前一个小时,dubbo这个问题可把我折腾惨了_第11张图片

上线前一个小时,dubbo这个问题可把我折腾惨了_第12张图片
如有收获,点个在看,诚挚感谢

尹吉欢