如何处理dubbo反序列化失败之后留下的坑,点开看看

2020-09-18 10:13:33 浏览数 (1)

写在前面:2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有需要学习的朋友可以Star一下! GitHub地址:https://github.com/abel-max/Java-Study-Note/tree/master

前言

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

场景还原

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

代码语言:javascript复制
{
  "code": "010000",
  "message":"java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee",
  "data": null
}

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

如何处理dubbo反序列化失败之后留下的坑,点开看看

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

AddEmployeeForm****类

代码语言:javascript复制
@Data
public class AddEmployeeForm implements Serializable {
    /**
     * 职员信息列表
     */
    private List<Employee> employees;
    @Data    public static class Employee implements Serializable {
        /**
         * 姓名
         */
        private String name;
        /**
         * 工作
         */
        private String job;
    }}

FormToDTO()方法

代码语言:javascript复制
public <T, F> T formToDTO(F form, T dto) {
    // 进行数据拷贝
    BeanUtils.copyProperties(form, dto);
    // 返回数据
    return dto;
}

AddEmployeeDTO类

代码语言:javascript复制
@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 进行实现,没使用过的小伙伴可以简单的了解下。

代码语言:javascript复制
@Mapper
public interface EmployeeConvert {
    EmployeeConvert INSTANCE = Mappers.getMapper(EmployeeConvert.class);
            AddEmployeeXmlReq dtoToXmlReq(AddEmployeeDTO dto);
}

AddEmployeeXmlReq类

代码语言:javascript复制
@Data
public class AddEmployeeXmlReq implements Serializable {
    /**
     * 职员信息列表
     */
    private List<Employee> employees;
    @Data    public static class Employee implements Serializable {
        /**
         * 姓名
         */
        private String name;
        /**
         * 工作
         */
        private String job;
    }}

EmployeeController

代码语言:javascript复制
@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

代码语言:javascript复制
@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 处理前后。如果请求正常 日志打印的顺序应该是:

如何处理dubbo反序列化失败之后留下的坑,点开看看

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

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

找到出错的代码

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

如何处理dubbo反序列化失败之后留下的坑,点开看看

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

如何处理dubbo反序列化失败之后留下的坑,点开看看

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

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

如何处理dubbo反序列化失败之后留下的坑,点开看看

这个时候你会发现接收到的 AddEmployeeDTO.employees 内存储的并不是一个 AddEmployeeDTO

Employee 转换为 HashMap 了。从而导致了 java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee 异常的抛出。

如何处理dubbo反序列化失败之后留下的坑,点开看看

你以为结束了?

为啥 Dubbo 反序列化时会将 AddEmployeeDTO

Employee ,找不到 AddEmployeeForm

Employee ?

如何处理dubbo反序列化失败之后留下的坑,点开看看

如何处理dubbo反序列化失败之后留下的坑,点开看看

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

如何处理dubbo反序列化失败之后留下的坑,点开看看

呕吼,这下石锤了。原来是在 formToDTO 的时候出问题了。传递过去 AddEmployeeDTO 内部的 Employee 竟然变成了 AddEmployeeForm

Employee的原因了。审查一下 formToDTO 的代码看看为啥会发生这样的情况:

代码语言:javascript复制
public <T, F> T formToDTO(F form, T dto) {
    // 进行数据拷贝
    BeanUtils.copyProperties(form, dto);
    // 返回数据
    return dto;
}

fromToDTO 内的代码非常精简,就一个 BeanUtils.copyProperties() 的方法,那毫无疑问它就是罪魁祸首了。通过在baidu的海洋里遨游,我找到了原因。原来是 BeanUtils 是浅拷贝造成的。浅拷贝只是调用子对象的set方法,并没有将所有属性拷贝。(也就是说,引用的一个内存地址),所以在转换的时候,将 AddEmployeeDTO 内的 employees 属性指向了 AddEmployeeForm的 employees 的内存地址。所以将在进行调用时, 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实现进行转换。
代码语言:javascript复制
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. 封装一个转换类进行转换。
代码语言:javascript复制
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。

0 人点赞