Beanutils造成dubbo反序列化失败?

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

前言

  今天下午,当我经过一个小时的奋”键“疾”码“,准备好好的审查一下(摸鱼)自己写的代码,经过一段时间审查(摸的差不多了,该下班了),得出一个结论我写的代码很优雅、精简。所以大手一挥提交代码,并在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进行参数传递。毕竟我都是个老手了,咋可能犯这样愚蠢的错误。俗话说遇到问题不要慌,让我们掏出手机先发个朋友圈,不对好像有点跑题了,我们先看一下调用链的数据传递。

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

AddEmployeeForm类

@Data
public class AddEmployeeForm implements Serializable {
   
   

    /**
     * 职员信息列表
     */
    private List<Employee> employees;

    @Data
    public static class Employee implements Serializable {
   
   

        /**
         * 姓名
         */
        private String name;

        /**
         * 工作
         */
        private String job;

    }
}

FormToDTO()方法

public <T, F> T formToDTO(F form, T dto) {
   
   

    // 进行数据拷贝
    BeanUtils.copyProperties(form, dto);

    // 返回数据
    return dto;
}

AddEmployeeDTO类

@Data
public class AddEmployeeDTO implements Serializable {
   
   

    /**
     * 职员信息列表
     */
    private List<Employee> employees;

    @Data
    public 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<Employee> employees;

    @Data
    public 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处理前后。如果请求正常 日志打印的顺序应该是:

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

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

找到出错的代码

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

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

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

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

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

你以为结束了?

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

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

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

public <T, F> 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()));
    
  3. 封装一个转换类进行转换。

AddEmployeeDTO dto = new AddEmployeeDTO();
dto.setEmployees(convertList(form.getEmployees(),AddEmployeeDTO.Employee.class));

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

总结

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

参考

结尾

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

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

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
存储 Dubbo Java
dubbo 源码 v2.7 分析:通信过程及序列化协议
前面我们介绍了dubbo的核心机制,今天将开始分析远程调用流程。毕竟,作为一个rpc框架,远程调用是理论的核心内容。通过对dubbo相关实现的探究,深入了解rpc原理及可能的问题。
189 0
|
安全 Dubbo 应用服务中间件
Dubbo反序列化漏洞复现分析(二)
Dubbo反序列化漏洞复现分析(二)
|
负载均衡 Dubbo 安全
Dubbo反序列化漏洞复现分析
Dubbo反序列化漏洞复现分析
|
Dubbo 应用服务中间件
Dubbo使用Hessian2序列化时针对Byte类型出现java.lang.ClassCastException
Dubbo使用Hessian2序列化时针对Byte类型出现java.lang.ClassCastException
182 0
|
Dubbo Java 应用服务中间件
关于在dubbo分布式种实体类Bean序列化产生的问题和内部类序列化的问题 UserServiceImpl must implement java.io.Serializable
今天在学习RPC dubbo的分布式 在跟老师学习的过程中,产生一个运行时报错,首先看项目结构和关键代码
302 0
关于在dubbo分布式种实体类Bean序列化产生的问题和内部类序列化的问题 UserServiceImpl must implement java.io.Serializable
|
机器学习/深度学习 设计模式 编解码
dubbo的编解码,序列化和通信
dubbo的编解码,序列化和通信
179 0
|
Dubbo Java 应用服务中间件
Dubbo3实践:基于 IDL 的 Triple 协议 Pojo 序列化兼容模式
这篇教程会通过从零构建一个简单的工程来演示如何基于 POJO 方式使用 Dubbo Triple, 在应用不改变已有接口定义的同时升级到 Triple 协议。**此模式下 Triple 使用方式与 Dubbo 协议一样。** 具体用例可以参考:[dubbo-samples-triple/pojo](https://github.com/apache/dubbo-samples/tree/mast
395 0
|
JSON Dubbo 网络协议
|
安全 Dubbo Java
Apacche Dubbo 反序列化漏洞
Apacche Dubbo 反序列化漏洞
375 0
Apacche Dubbo 反序列化漏洞
|
Dubbo 算法 Java
Dubbo的多种序列化算法(下)
Dubbo的多种序列化算法
190 0
Dubbo的多种序列化算法(下)
下一篇
无影云桌面