map insert异常失败_处理dubbo反序列化失败的坑

cd0e821bc8470f0fbb5ffb6ab4adcdf7.png

前言

  今天下午,当我经过一个小时的奋”键“疾”码“,准备好好的审查一下(摸鱼)自己写的代码,经过一段时间审查(摸的差不多了,该下班了),得出一个结论我写的代码很优雅、精简。所以大手一挥提交代码,并在 API 管理系统上将 xxx 接口点了个完成。准备收拾东西走人了准点下班。然而事与愿违,没过多久前端大哥就@我了,说 xxx 接口有问题,麻烦处理一下。内心第一反应(你丫的参数传错了吧)卑微的我只能默默的回个,好的、麻烦把参数给我一下,我这边检查一下[微笑脸]。

场景还原

  经过测试,发现确实是我的问题。还好没甩锅,要不然就要被打脸了。错误信息如下:

{
  "code": "010000",
  "message":"java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee",
  "data": null
}

  看到这个错误有点懵,HashMap 无法转换为AddEmployeeDTO$Employee。内心在想,没道理啊。请求参数我都是拷贝过来的,压根就没用Map进行参数传递。毕竟我都是个老手了,咋可能犯这样愚蠢的错误。俗话说遇到问题不要慌,让我们掏出手机先发个朋友圈,不对好像有点跑题了,我们先看一下调用链的数据传递。

355421b61ce996d036095cf326dbfc18.png

  首先 web 将AddEmployeeForm数据传递到服务端,然后使用fromToDTO()方法,进行将数据转换为 Dubbo 请求需要的AddEmployeeDTO。Dubbo 服务放接收AddEmployeeDTO后,使用 EmployeeConvert 将数据转换为AddEmployeeXmlReq再执行相关逻辑。

AddEmployeeForm 类

@Data
public class AddEmployeeForm implements Serializable {

    /**
     * 职员信息列表
     */
    private List employees;@Datapublic static class Employee implements Serializable {/**
         * 姓名
         */private String name;/**
         * 工作
         */private String job;
    }
}

FormToDTO()方法

public  T formToDTO(F form, T dto) {// 进行数据拷贝
    BeanUtils.copyProperties(form, dto);// 返回数据return dto;
}

AddEmployeeDTO 类

@Data
public class AddEmployeeDTO implements Serializable {

    /**
     * 职员信息列表
     */
    private List employees;@Datapublic static class Employee implements Serializable {/**
         * 姓名
         */private String name;/**
         * 工作
         */private String job;
    }
}

EmployeeConvert 转换类

EmployeeConvert 转换类,使用了mapstruct进行实现,没使用过的小伙伴可以简单的了解下。

@Mapper
public interface EmployeeConvert {

    EmployeeConvert INSTANCE = Mappers.getMapper(EmployeeConvert.class);

    AddEmployeeXmlReq dtoToXmlReq(AddEmployeeDTO dto);

}

AddEmployeeXmlReq 类

@Data
public class AddEmployeeXmlReq implements Serializable {

    /**
     * 职员信息列表
     */
    private List employees;@Datapublic static class Employee implements Serializable {/**
         * 姓名
         */private String name;/**
         * 工作
         */private String job;
    }
}

EmployeeController

@RestController
@AllArgsConstructor
public class EmployeeController {

    private final EmployeeRpcProvider provider;

    @PostMapping("/employee/add")
    public ResultVO employeeAdd(@RequestBody AddEmployeeForm form) {
        provider.add(formToDTO(form,new AddEmployeeDTO()));
        return ResultUtil.success();
    }
}

EmployeeRpcServiceImpl

@Slf4j
@Service
public class EmployeeRpcServiceImpl implements EmployeeService {

    @Override
    public ResultDTO add(AddEmployeeDTO dto) {
        log.info("dubbo-provider-AddEmployeeDTO:{}", JSON.toJSONString(dto));
        AddEmployeeXmlReq addEmployeeXmlReq = EmployeeConvert.INSTANCE.dtoToXmlReq(dto);
        return ResultUtil.success();
    }
}

分析原因

判断异常抛出点

  我们需要先确定异常是在consumer 抛出的还是provider抛出的。判断过程很简单,我们可以进行本地debug,看看是执行到哪里失败了就知道了。如果不方便本地调试,我们可以在关键点上打上相应的日志。比如说consumer调用前后,provider处理前后。如果请求正常 日志打印的顺序应该是:

8435b21744e6ecddb16bfb77e7eabc61.png

这样通过观察日志就可以判定异常是在哪里抛出的了。

实际并没有这样麻烦,因为在 consumer 做了 rpc 异常拦截,所以我当时看了下 consumer 的日志就知道是 provider 抛出来的。

找到出错的代码

  既然找到了出问题是出在provider,那看是什么原因导致的,从前面的调用链可以知道,provider接收到AddEmployeeDTO会使用EmployeeConvert将其转换为AddEmployeeXmlReq,所以我们可以打印出AddEmployeeDTO看看consumer的传参是否正常。

f6a10bc020bf1ce4a22965b3a4314ae4.png

  通过日志我们可以发现consumer将参数正常的传递过来了。那么问题应该就出在EmployeeConvertAddEmployeeDTO转换为AddEmployeeXmlReq这里了。由于EmployeeConvert是使用mapstruct进行实现,我们可以看看自动生成的转换类实现逻辑是咋样的。

307a9fca14226e69bb03cd04bc3208e3.png

  通过观察源代码可以发现,在进行转换的时候需要传入一个List 而这个Employee正是AddEmployeeDTO.Employee。这个时候可能会困扰了,我明明就是传入AddEmployeeDTO,而且类里面压根就没有Map,为啥会抛出java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee这个异常呢?

让我们Debug一下看看发生了啥。

0ac68ba4bd58598e9c41ab33cac780d2.png

  这个时候你会发现接收到的AddEmployeeDTO.employees内存储的并不是一个AddEmployeeDTO$Employee对象,而是一个HashMap。那看来真相大白了,原来是 dubbo 反序列化的时候将AddEmployeeDTO$Employee 转换为HashMap了。从而导致了java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee异常的抛出。

33bd5b0d763b35cecc804ff52b8c1011.gif

你以为结束了?

  为啥Dubbo反序列化时会将AddEmployeeDTO$Employee变成Map呢?我们回过头看看之前打印参数的日志,有一个警告日志提示了java.lang.ClassNotFoundException:com.aixiao.inv.api.model.form.AddEmployeeForm$Employee ,找不到AddEmployeeForm$Employee这个就有点奇怪了,为啥不是AddEmployeeDTO$Employee

f6a10bc020bf1ce4a22965b3a4314ae4.png
3a01b2c0113b4499019ef59fe6cdc58c.png

  在进行dubbo调用前AddEmployeeForm会使用fromToDTO()方法将其转化为AddEmployeeDTO。那么问题会不会出现在这里呢?我们继续Debug看看。

48f7673cb179e7cc90e150ce163c7596.png

  呕吼,这下石锤了。原来是在formToDTO的时候出问题了。传递过去AddEmployeeDTO内部的Employee竟然变成了AddEmployeeForm$Employee。这也是为什么provider那边会抛出java.lang.ClassNotFoundException:com.aixiao.inv.api.model.form.AddEmployeeForm$Employee的原因了。审查一下formToDTO的代码看看为啥会发生这样的情况:

public  T formToDTO(F form, T dto) {// 进行数据拷贝
    BeanUtils.copyProperties(form, dto);// 返回数据return dto;
}

  fromToDTO内的代码非常精简,就一个BeanUtils.copyProperties()的方法,那毫无疑问它就是罪魁祸首了。通过在 baidu 的海洋里遨游,我找到了原因。原来是BeanUtils是浅拷贝造成的。浅拷贝只是调用子对象的 set 方法,并没有将所有属性拷贝。(也就是说,引用的一个内存地址),所以在转换的时候,将AddEmployeeDTO内的employees属性指向了AddEmployeeFormemployees的内存地址。所以将在进行调用时,Dubbo因为反序列化时找不到对应的类,就会将其转换为Map

小结一下

  上面的问题,主要是由于 BeanUtils 浅拷贝造成。并且引发连锁反应,造成Dubbo反序列化异常以及EmployeeConvert的转换异常,最后抛出了java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee 错误信息。

解决方法

  既然知道了问题出现的原因,那么解决起来就很简单了。对于单一的属性,那么不涉及到深拷贝的问题,适合用 BeanUtils 继续进行拷贝。但是涉及到集合我们可以这样处理:

  1. 简单粗暴使用 foreach 进行拷贝。

  2. 使用 labmda 实现进行转换。

AddEmployeeDTO dto = new AddEmployeeDTO();
dto.setEmployees(form.getEmployees().stream().map(tmp -> {
  AddEmployeeDTO.Employee employee = new AddEmployeeDTO.Employee();
  BeanUtils.copyProperties(tmp,employee);
  return employee;
}).collect(Collectors.toList()));
  1. 封装一个转换类进行转换。
AddEmployeeDTO dto = new AddEmployeeDTO();
dto.setEmployees(convertList(form.getEmployees(),AddEmployeeDTO.Employee.class));

public  List convertList(List source, Class targetClass) {return JSON.parseArray(JSON.toJSONString(source), targetClass);
}

总结

  1. 使用 BeanUtils.copyProperties()进行拷贝需要注意
  2. dubbo 在进行反序列化的时候,如果找不到对应类会将其转化为 map。

参考

  • BeanUtils.copyProperties 的使用(深拷贝,浅拷贝)

结尾

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

27facedb934fb61ca378a113d6bc082d.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/505327.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python资源管理器选择文件_Python:在资源管理器中获取选定文件的列表(windows7)...

我知道现在在这里发布答案有点晚了,但我几个月前尝试过Olav的解决方案,但它没有完全起作用:工作目录是脚本的工作目录,所以我不得不删除if条件才能使其工作,但它选择了所有Windows资源管理器窗口中的所有文件&#xff…

计算机网络 --- 数据链路层CSMA/CA协议

CSMA/CA全称 载波监听多点接入/碰撞避免CSMA/CA(carrier sense multiple access with collision avoidence) CSMA/CA协议与CSMA/CD协议的主要区别在应用场景不同。 为什么要有CSMA/CA协议 CSMA/CA协议主要应用于无线局域网,CSMA/CD协议主…

class会不会回收?用不到的class怎么回收_牛X的java程序员必备的GC基础知识, 面试肯定用的到...

1. GC回收哪些内存区域呢?堆内存对象数组方法区该类所有的额实例都已经被回收, 也就是java堆中不存在该类的任何实例加载该类的ClassLoader已经被回收该类对应的java.lang.Class对象在任何地方没有被引用, 也无法通过反射访问该类方法。垃圾回…

c++读取utf8文件_Node.js 进阶之 fs 文件模块学习

前言文件操作是开发过程中并不可少的一部分。Node.js 中的 fs 模块是文件操作的封装,它提供了文件读取、写入、更名、删除、遍历目录、链接等 POSIX 文件系统操作。与其它模块不同的是,fs 模块中所有的操作都提供了异步和同步的两个版本,具有 sync 后缀的…

计算机网络 --- 数据链路层中局域网和广域网

局域网 简称LAN(Local Area Network):是指在某一区域内由多台计算机互联成的计算机组,使用广播信道。 局域网的特点 覆盖的地理范围较小,只在一个相对独立的局部范围内联,如一座或集中的建筑群内。使用专…

python pathname_Python模块的定义,模块的导入,__name__用法实例分析

模块导入:import 模块名pythonimport 模块名import mathprint(math.sqrt(9))from…import 语句:从模块中导入一个指定的部分,如类、方法,其中from…import * 代表导入全部内容python#from 模块名 import 类名、方法名from collections import Iterablepr…

python制作中文词云_Python如何生成词云(详解)

前言 今天教大家用wrodcloud模块来生成词云,我读取了一篇小说并生成了词云,先看一下效果图: 效果图一:效果图二:根据效果图分析的还是比较准确的,小说中的主人公就是“程理”,所以出现次数最多。…

计算机网络 --- 局域网中的以太网

以太网概述 以太网是应用最广泛的局域网,包括标准以太网(10Mbps),快速以太网(1000Mbps),千兆以太网(1000Mbps)和10G以太网,他们都符合IEEE802.3系列标准规范…

高度为5的3阶b树含有的关键字个数_数据结构要考得好,你心里要有B树

01知识框架02知识点详解1B树①定义与性质B树也叫B-树。B树是一种平衡的多分树,通常我们说m阶的B树,是二叉排序树的一种扩展,它必须满足如下条件:01每个结点最多只有m-1个关键字。02根结点最少可以只有1个关键字。03非根结点至少有…

计算机网络 --- 数据交换方式

电路交换 电话网络就是一种电路交换 电路交换的阶段 建立连接(呼叫/电路建立)通信释放连接 电路交换的优点 通信时延小有序传输没有冲突实时性强 电路交换的缺点 建立连接时间长线路独占,使用效率低灵活性差无差错控制能力 报文交换 报…

科沃斯机器人拆解_科沃斯扫地机器人拆解/

5、扫地机器人十大排名品牌第五名:(科沃斯中国)科沃斯扫地机器人科沃斯机器人科技(苏州)有限公司,扫地机器人十大品牌,国家火炬计...扫地机器人尘盒大多都是放置在机体内部的,因此在拆卸扫地机器人尘盒的时候,应该先查…

python json模块详解_深入解析Python编程中JSON模块的使用

JSON编码支持的基本数据类型为 None , bool , int , float 和 str , 以及包含这些类型数据的lists,tuples和dictionaries。 对于dictionaries,keys需要是字符串类型(字典中任何非字符串类型的key在编码时会…

计算机网络 --- 网络层路由算法与路由协议

路由表: 最佳路由: “最佳”只能是相对于某一种特定要求下得出的较为合理的选择而已。 路由算法的分类 静态路由算法(非自适应路由算法) 管理员手工配置路由信息。简便,可靠,在负荷稳定,拓扑…

同比 数据模型 环比_历史数据模型解决方案-历史数据模型解决方案

方案介绍针对上面介绍常规实现方案的弊端,本文从数据结构设计入手提出了“历史数据模型”解决方案。在物理表设计阶段针对这类报表问题进行特殊处理,物理表数据结构设计如下:字段描述IdIDProject项目Amount本月投资额TotalAmount累计投资额(截…

cplex安装_Excel软件规划求解工具的安装与功能介绍

引言规划求解工具是Excel软件中自带的一个功能非常强大的加载项/工具。它能够完成包括线性规划、整数线性规划等一般规模的数学优化问题的问题求解。更重要的是,无论是在Windows操作系统下,还是苹果电脑Mac平台下,这个工具都是免费的。下面就…

python二进制文件 删除尾部数据_在Python中读取和切片二进制数据文件的最快方法...

我有一个处理脚本,用于提取“uint16”类型的二进制数据文件,并一次以6400块的形式进行各种处理.该代码最初是用Matlab编写的,但由于分析代码是用 Python编写的,我们希望通过在Python中完成所有工作来简化流程.问题是我注意到我的Python代码比Matlab的fread函数慢得多.简单地说,…

计算机网络 --- 网络层IP数据报

IP数据报格式 首部 版本:IPv4/IPv6首部长度:单位是4B,最小为5。也就是说如果首部长度的四个bit的出来的数是8,那么首部长度就是8 * 4B 32B也就是32字节区分服务:指示期望获得哪种类型的服务总长度:首部数据…

线程管理(学习)

线程管理 在日常生活中,我们要完成一个大任务,一般会将它分解成多个简单、容易解决的小问题,小问题逐个被解决,大问题也就随之解决了。 在多线程操作系统中,也同样需要开发人员把一个复杂的应用分解成多个小的、可调…

imread函数_MATLAB图像处理:27:使用imtranslate函数平移图像

本示例说明如何使用imtranslate函数对图像执行平移操作。平移操作将图像在x或y方向或两者上移动指定数量的像素。将图像读入工作区。I imread(cameraman.tif);显示图像。图像的大小为256 x 256像素。默认情况下,imshow显示图像的左上角坐标为(0,0&#…

ads design environment_ADS应用技巧3 — 画一个巴伦有多少种方法?

为什么ADS的Schematic不允许多任务仿真(即如果一个Schematic的仿真任务没完成,就不允许任何Schematic启动新的仿真);而Layout却允许同时运行一大堆EM仿真。按理说,Schematic仿真相比于EM仿真占用更少的CPU和RAM等计算机资源,更应该…