我去,又遇到 Dubbo 反序列化失败的坑!!
Java技术栈
www.javastack.cn
关注阅读更多优质文章
前言
场景还原
{
"code": "010000",
"message":"java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee",
"data": null
}
看到这个错误有点懵,HashMap 无法转换为AddEmployeeDTO$Employee。
首先 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将参数正常的传递过来了。那么问题应该就出在EmployeeConvert将AddEmployeeDTO转换为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()的方法,那毫无疑问它就是罪魁祸首了。
小结一下
上面的问题,主要是由于 BeanUtils 浅拷贝造成。并且引发连锁反应,造成Dubbo反序列化异常以及EmployeeConvert的转换异常,最后抛出了java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee 错误信息。
解决方法
既然知道了问题出现的原因,那么解决起来就很简单了。对于单一的属性,那么不涉及到深拷贝的问题,适合用 BeanUtils 继续进行拷贝。但是涉及到集合我们可以这样处理:
-
简单粗暴使用 foreach 进行拷贝。
-
使用 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()));
-
封装一个转换类进行转换。
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);
}
总结
-
使用 BeanUtils.copyProperties()进行拷贝需要注意 -
dubbo 在进行反序列化的时候,如果找不到对应类会将其转化为 map。
参考
关注Java技术栈看更多干货
