乐趣区

关于java:处理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 进行参数传递。毕竟我都是个新手了,咋可能犯这样愚昧的谬误。俗话说遇到问题不要慌,让咱们掏出手机先发个朋友圈,不对如同有点跑题了,咱们先看一下调用链的数据传递。

  首先 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()));
  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 的应用(深拷贝,浅拷贝)

结尾

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

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

退出移动版