前言

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

@Datapublic 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类

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

EmployeeConvert转换类

EmployeeConvert转换类,应用了mapstruct进行实现,没应用过的小伙伴能够简略的理解下。
@Mapperpublic interface EmployeeConvert {    EmployeeConvert INSTANCE = Mappers.getMapper(EmployeeConvert.class);            AddEmployeeXmlReq dtoToXmlReq(AddEmployeeDTO dto);}

AddEmployeeXmlReq类

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

EmployeeController

@RestController@AllArgsConstructorpublic 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@Servicepublic 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()));
  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。

参考

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

结尾

  我是不一样的科技宅,每天提高一点点,体验不一样的生存。咱们下期见!

  如果感觉对你有帮忙,能够多多评论,多多点赞哦,也能够到我的主页看看,说不定有你喜爱的文章,也能够顺手点个关注哦,谢谢。