
前言
今天下午,當我經過一個小時的奮”鍵“疾”碼“,準備好好的審查一下(摸魚)自己寫的代碼,經過一段時間審查(摸的差不多了,該下班了),得出一個結論我寫的代碼很優雅、精簡。是以大手一揮送出代碼,并在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
将參數正常的傳遞過來了。那麼問題應該就出在
EmployeeConvert
将
AddEmployeeDTO
轉換為
AddEmployeeXmlReq
這裡了。由于
EmployeeConvert
是使用
進行實作,我們可以看看自動生成的轉換類實作邏輯是咋樣的。
通過觀察源代碼可以發現,在進行轉換的時候需要傳入一個
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
屬性指向了
AddEmployeeForm
的
employees
的記憶體位址。是以将在進行調用時,
Dubbo
因為反序列化時找不到對應的類,就會将其轉換為
Map
。
小結一下
上面的問題,主要是由于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。
參考
結尾
我是不一樣的科技宅,每天進步一點點,體驗不一樣的生活。我們下期見!
如果覺得對你有幫助,可以多多評論,多多點贊哦,也可以到我的首頁看看,說不定有你喜歡的文章,也可以随手點個關注哦,謝謝。