天天看点

Beanutils造成dubbo反序列化失败?

Beanutils造成dubbo反序列化失败?

前言

  今天下午,当我经过一个小时的奋”键“疾”码“,准备好好的审查一下(摸鱼)自己写的代码,经过一段时间审查(摸的差不多了,该下班了),得出一个结论我写的代码很优雅、精简。所以大手一挥提交代码,并在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

进行参数传递。毕竟我都是个老手了,咋可能犯这样愚蠢的错误。俗话说遇到问题不要慌,让我们掏出手机先发个朋友圈,不对好像有点跑题了,我们先看一下调用链的数据传递。

Beanutils造成dubbo反序列化失败?

  首先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

处理前后。如果请求正常 日志打印的顺序应该是:

Beanutils造成dubbo反序列化失败?

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

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

找到出错的代码

  既然找到了出问题是出在

provider

,那看是什么原因导致的,从前面的调用链可以知道,

provider

接收到

AddEmployeeDTO

会使用

EmployeeConvert

将其转换为

AddEmployeeXmlReq

,所以我们可以打印出

AddEmployeeDTO

看看

consumer

的传参是否正常。

Beanutils造成dubbo反序列化失败?

  通过日志我们可以发现

consumer

将参数正常的传递过来了。那么问题应该就出在

EmployeeConvert

AddEmployeeDTO

转换为

AddEmployeeXmlReq

这里了。由于

EmployeeConvert

是使用

进行实现,我们可以看看自动生成的转换类实现逻辑是咋样的。

Beanutils造成dubbo反序列化失败?

  通过观察源代码可以发现,在进行转换的时候需要传入一个

List<Employee>

而这个

Employee

正是

AddEmployeeDTO.Employee

。这个时候可能会困扰了,我明明就是传入

AddEmployeeDTO

,而且类里面压根就没有

Map

,为啥会抛出

java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee

这个异常呢?

让我们

Debug

一下看看发生了啥。

Beanutils造成dubbo反序列化失败?

  这个时候你会发现接收到的

AddEmployeeDTO.employees

内存储的并不是一个

AddEmployeeDTO$Employee

对象,而是一个

HashMap

。那看来真相大白了,原来是dubbo反序列化的时候将

AddEmployeeDTO$Employee

HashMap

了。从而导致了

java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee

异常的抛出。

Beanutils造成dubbo反序列化失败?

你以为结束了?

  为啥

Dubbo

反序列化时会将

AddEmployeeDTO$Employee

变成

Map

呢?我们回过头看看之前打印参数的日志,有一个警告日志提示了

java.lang.ClassNotFoundException:com.aixiao.inv.api.model.form.AddEmployeeForm$Employee

,找不到

AddEmployeeForm$Employee

这个就有点奇怪了,为啥不是

AddEmployeeDTO$Employee

Beanutils造成dubbo反序列化失败?
Beanutils造成dubbo反序列化失败?

  在进行

dubbo

调用前

AddEmployeeForm

fromToDTO()

方法将其转化为

AddEmployeeDTO

。那么问题会不会出现在这里呢?我们继续

Debug

看看。

Beanutils造成dubbo反序列化失败?

  呕吼,这下石锤了。原来是在

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

属性指向了

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实现进行转换。
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 <S, T> List<T> convertList(List<S> source, Class<T> targetClass) {
return JSON.parseArray(JSON.toJSONString(source), targetClass);
}           

总结

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

参考

结尾

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

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

继续阅读