前因

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

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

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

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

第一步:找到错误信息

机智的我在第一时间打开了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<String> 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> 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了,这才有了我们前面的错误。

结局

找到原因后解决就是分分钟的事了,通过这个问题还是说明了加任何的代码都有风险。剩下的就是开发的锅了,加了代码没有自测,好在有测试把关,否则就凉凉了。

上线前一个小时,dubbo这个问题可把我折腾惨了的更多相关文章

  1. mysql 获取昨天日期、今天日期、明天日期以及前一个小时和后一个小时的时间

    1.当前日期 select DATE_SUB(curdate(),INTERVAL 0 DAY) ; 2.明天日期select DATE_SUB(curdate(),INTERVAL -1 DAY) ...

  2. Java 获取当前时间前一个小时的时间

    /** * 获取当前时间前一个小时的时间 */ public static void beforeOneHourToNowDate() { Calendar c = new Calendar.getI ...

  3. BGV上线17小时最高888.88美金,投资最高回报率近+1778倍, 带动NGK内存暴涨

    至12月3日BGV币上线A网交易所DeFi板块以来,BGV价值飙升长.,据非小号的数据显示,BGV币价是718美元(东八区时间2020年12月4日早上九点四十),相较昨日涨幅达70.14%,以718美 ...

  4. svn版本管理与上线

    1.1 SVN介绍 1.1.1 什么是SVN(Subversion)? Svn(subversion)是近年来崛起的非常优秀的版本管理工具,与CVS管理工具一样,SVN是一个跨平台的开源的版本控制系统 ...

  5. Linux实战教学笔记41:企业级SVN版本管理与大型代码上线方案

    第1章 SVN服务实战应用指南 1.1 SVN介绍 1.1.1 什么是SVN(Subversion)? Svn(subversion)是近年来崛起的非常优秀的版本管理工具,与CVS管理工具一样,SVN ...

  6. CP干货:手机游戏上线前需要准备什么

    转自:http://www.gamelook.com.cn/2015/09/229002 游戏研发完成后游戏该怎样推广?如何找渠道?推广时需要注意什么?下面给大家介绍一下具体流程,可能每个公司的上线流 ...

  7. SVN版本管理与大型代码上线方案(一)

    SVN版本管理与大型代码上线方案(一) 链接:https://pan.baidu.com/s/1A3Iq3gGkGS27L_Gt37_I0g 提取码:ncy2 复制这段内容后打开百度网盘手机App,操 ...

  8. 总结:Unity3D游戏上线后的流程回顾

    原地址:http://unity3d.9tech.cn/news/2014/0127/39748.html 首先.unity 灯光烘焙 :Unity 3d FBX模型导入.选项Model 不导入资源球 ...

  9. php获取前一天,前一个月,前一年的时间

    获取前一天的时间: $mytime= date("Y-m-d H:i:s", strtotime("-1 day")); 获取三天前的时间: $mytime= ...

随机推荐

  1. Keras lstm 文本分类示例

    #基于IMDB数据集的简单文本分类任务 #一层embedding层+一层lstm层+一层全连接层 #基于Keras 2.1.1 Tensorflow 1.4.0 代码: '''Trains an LS ...

  2. scrf 原理及flask-wtf防护

    了解什么是scrf? SCRF跨站点请求伪造Cross—Site Request Forgery) 指恶意用户通过个人用户的点击,然而盗用用户的账号信息,并发送邮件.虚拟货币的转账,以及一些重要的事务 ...

  3. java基础 -- 关键字static的用法

    static关键字的基本作用就是方便在没有创建对象的情况下调用类的方法/变量, static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问. static ...

  4. 优雅的使用 ThreadLocal

    前言 在我们日常 Java Web 开发中难免遇到需要把一个参数层层的传递到最内层,然后中间层根本不需要使用这个参数,或者是仅仅在特定的工具类中使用,这样我们完全没有必要在每一个方法里面都传递这样一个 ...

  5. 搜索排序-learning to Rank简介

    Learning to Rank pointwise \[ L\left(f ; x_{j}, y_{j}\right)=\left(y_{j}-f\left(x_{j}\right)\right)^ ...

  6. python+selenium+Chrome options参数

    python+selenium+Chrome options参数 Chrome Options常用的行为一般有以下几种: 禁止图片和视频的加载:提升网页加载速度. 添加代理:用于翻墙访问某些页面,或者 ...

  7. 洛谷P1220 关路灯 题解 区间DP

    题目链接:https://www.luogu.com.cn/problem/P1220 本题涉及算法:区间DP. 我们一开始要做一些初始化操作,令: \(p[i]\) 表示第i个路灯的位置: \(w[ ...

  8. 从N个元素中抽取K个不重复元素(抽奖问题)

    核心就是 把N数组抽中的元素给K数组 把N数组最后一位给N数组被抽走的那一位(这时候N数组最后一位元素和被抽走的那位元素值相等) 把N数组长度减一,去除最后一位

  9. Quartz 和 springboot schedule中的cron表达式关于星期(周几)的不同表示

    一.Quartz中cron 表达式分析: quartz 官方源码(org.quartz.CronExpression)解释: Cron expressions are comprised of 6 r ...

  10. VS从标准输入读入文件

    1.点击[生成],在对应目标平台[64 or 32]文件夹下的[release]或[debug]下找到可执行文件 2.读取销售记录文件 1)打开cmd,将销售记录文件和可执行文件放在同一文件夹下 2) ...