
前言
??今天下午,當我經(jīng)過一個小時的奮”鍵“疾”碼“,準備好好的審查一下(摸魚)自己寫的代碼,經(jīng)過一段時間審查(摸的差不多了,該下班了),得出一個結(jié)論我寫的代碼很優(yōu)雅、精簡。所以大手一揮提交代碼,并在API管理系統(tǒng)上將xxx接口點了個完成。準備收拾東西走人了準點下班。然而事與愿違,沒過多久前端大哥就@我了,說xxx接口有問題,麻煩處理一下。內(nèi)心第一反應(你丫的參數(shù)傳錯了吧)卑微的我只能默默的回個,好的、麻煩把參數(shù)給我一下,我這邊檢查一下[微笑臉]。
場景還原
??經(jīng)過測試,發(fā)現(xiàn)確實是我的問題。還好沒甩鍋,要不然就要被打臉了。錯誤信息如下:
{
"code": "010000",
"message":"java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee",
"data": null
}
??看到這個錯誤有點懵,HashMap 無法轉(zhuǎn)換為AddEmployeeDTO$Employee。內(nèi)心在想,沒道理啊。請求參數(shù)我都是拷貝過來的,壓根就沒用Map進行參數(shù)傳遞。畢竟我都是個老手了,咋可能犯這樣愚蠢的錯誤。俗話說遇到問題不要慌,讓我們掏出手機先發(fā)個朋友圈,不對好像有點跑題了,我們先看一下調(diào)用鏈的數(shù)據(jù)傳遞。

??首先web將AddEmployeeForm數(shù)據(jù)傳遞到服務端,然后使用fromToDTO()方法,進行將數(shù)據(jù)轉(zhuǎn)換為Dubbo請求需要的AddEmployeeDTO。Dubbo服務放接收AddEmployeeDTO后,使用 EmployeeConvert 將數(shù)據(jù)轉(zhuǎn)換為AddEmployeeXmlReq再執(zhí)行相關(guān)邏輯。
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) {
// 進行數(shù)據(jù)拷貝
BeanUtils.copyProperties(form, dto);
// 返回數(shù)據(jù)
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轉(zhuǎn)換類
EmployeeConvert轉(zhuǎn)換類,使用了mapstruct進行實現(xiàn),沒使用過的小伙伴可以簡單的了解下。
@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,看看是執(zhí)行到哪里失敗了就知道了。如果不方便本地調(diào)試,我們可以在關(guān)鍵點上打上相應的日志。比如說consumer調(diào)用前后,provider處理前后。如果請求正常 日志打印的順序應該是:

這樣通過觀察日志就可以判定異常是在哪里拋出的了。
實際并沒有這樣麻煩,因為在consumer做了rpc異常攔截,所以我當時看了下consumer的日志就知道是provider拋出來的。
找到出錯的代碼
??既然找到了出問題是出在provider,那看是什么原因?qū)е碌?,從前面的調(diào)用鏈可以知道,provider接收到AddEmployeeDTO會使用EmployeeConvert將其轉(zhuǎn)換為AddEmployeeXmlReq,所以我們可以打印出AddEmployeeDTO看看consumer的傳參是否正常。

??通過日志我們可以發(fā)現(xiàn)consumer將參數(shù)正常的傳遞過來了。那么問題應該就出在EmployeeConvert將AddEmployeeDTO轉(zhuǎn)換為AddEmployeeXmlReq這里了。由于EmployeeConvert是使用mapstruct進行實現(xiàn),我們可以看看自動生成的轉(zhuǎn)換類實現(xiàn)邏輯是咋樣的。

??通過觀察源代碼可以發(fā)現(xiàn),在進行轉(zhuǎn)換的時候需要傳入一個List<Employee> 而這個Employee正是AddEmployeeDTO.Employee。這個時候可能會困擾了,我明明就是傳入AddEmployeeDTO,而且類里面壓根就沒有Map,為啥會拋出java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee這個異常呢?
讓我們Debug一下看看發(fā)生了啥。

??這個時候你會發(fā)現(xiàn)接收到的AddEmployeeDTO.employees內(nèi)存儲的并不是一個AddEmployeeDTO$Employee對象,而是一個HashMap。那看來真相大白了,原來是dubbo反序列化的時候?qū)?code>AddEmployeeDTO$Employee 轉(zhuǎn)換為HashMap了。從而導致了java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee異常的拋出。

你以為結(jié)束了?
??為啥Dubbo反序列化時會將AddEmployeeDTO$Employee變成Map呢?我們回過頭看看之前打印參數(shù)的日志,有一個警告日志提示了java.lang.ClassNotFoundException:com.aixiao.inv.api.model.form.AddEmployeeForm$Employee ,找不到AddEmployeeForm$Employee這個就有點奇怪了,為啥不是AddEmployeeDTO$Employee?


??在進行dubbo調(diào)用前AddEmployeeForm會使用fromToDTO()方法將其轉(zhuǎn)化為AddEmployeeDTO。那么問題會不會出現(xiàn)在這里呢?我們繼續(xù)Debug看看。

??嘔吼,這下石錘了。原來是在formToDTO的時候出問題了。傳遞過去AddEmployeeDTO內(nèi)部的Employee竟然變成了AddEmployeeForm$Employee。這也是為什么provider那邊會拋出java.lang.ClassNotFoundException:com.aixiao.inv.api.model.form.AddEmployeeForm$Employee的原因了。審查一下formToDTO的代碼看看為啥會發(fā)生這樣的情況:
public <T, F> T formToDTO(F form, T dto) {
// 進行數(shù)據(jù)拷貝
BeanUtils.copyProperties(form, dto);
// 返回數(shù)據(jù)
return dto;
}
??fromToDTO內(nèi)的代碼非常精簡,就一個BeanUtils.copyProperties()的方法,那毫無疑問它就是罪魁禍首了。通過在baidu的海洋里遨游,我找到了原因。原來是BeanUtils是淺拷貝造成的。淺拷貝只是調(diào)用子對象的set方法,并沒有將所有屬性拷貝。(也就是說,引用的一個內(nèi)存地址),所以在轉(zhuǎn)換的時候,將AddEmployeeDTO內(nèi)的employees屬性指向了AddEmployeeForm的employees的內(nèi)存地址。所以將在進行調(diào)用時,Dubbo因為反序列化時找不到對應的類,就會將其轉(zhuǎn)換為Map。
小結(jié)一下
??上面的問題,主要是由于BeanUtils淺拷貝造成。并且引發(fā)連鎖反應,造成Dubbo反序列化異常以及EmployeeConvert的轉(zhuǎn)換異常,最后拋出了java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee 錯誤信息。
解決方法
??既然知道了問題出現(xiàn)的原因,那么解決起來就很簡單了。對于單一的屬性,那么不涉及到深拷貝的問題,適合用BeanUtils繼續(xù)進行拷貝。但是涉及到集合我們可以這樣處理:
簡單粗暴使用foreach進行拷貝。
使用labmda實現(xiàn)進行轉(zhuǎn)換。
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()));
- 封裝一個轉(zhuǎn)換類進行轉(zhuǎn)換。
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);
}
總結(jié)
- 使用BeanUtils.copyProperties()進行拷貝需要注意
- dubbo在進行反序列化的時候,如果找不到對應類會將其轉(zhuǎn)化為map。
參考
結(jié)尾
??如果覺得對你有幫助,可以多多評論,多多點贊哦,也可以到我的主頁看看,說不定有你喜歡的文章,也可以隨手點個關(guān)注哦,謝謝。
??我是不一樣的科技宅,每天進步一點點,體驗不一樣的生活。我們下期見!