天天看點

Beanutils造成dubbo反序列化失敗?

Beanutils造成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

進行參數傳遞。畢竟我都是個老手了,咋可能犯這樣愚蠢的錯誤。俗話說遇到問題不要慌,讓我們掏出手機先發個朋友圈,不對好像有點跑題了,我們先看一下調用鍊的資料傳遞。

Beanutils造成dubbo反序列化失敗?

  首先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

處理前後。如果請求正常 日志列印的順序應該是:

Beanutils造成dubbo反序列化失敗?

這樣通過觀察日志就可以判定異常是在哪裡抛出的了。

實際并沒有這樣麻煩,因為在consumer做了rpc異常攔截,是以我當時看了下consumer的日志就知道是provider抛出來的。

找到出錯的代碼

  既然找到了出問題是出在

provider

,那看是什麼原因導緻的,從前面的調用鍊可以知道,

provider

接收到

AddEmployeeDTO

會使用

EmployeeConvert

将其轉換為

AddEmployeeXmlReq

,是以我們可以列印出

AddEmployeeDTO

看看

consumer

的傳參是否正常。

Beanutils造成dubbo反序列化失敗?

  通過日志我們可以發現

consumer

将參數正常的傳遞過來了。那麼問題應該就出在

EmployeeConvert

AddEmployeeDTO

轉換為

AddEmployeeXmlReq

這裡了。由于

EmployeeConvert

是使用

進行實作,我們可以看看自動生成的轉換類實作邏輯是咋樣的。

Beanutils造成dubbo反序列化失敗?

  通過觀察源代碼可以發現,在進行轉換的時候需要傳入一個

List<Employee>

而這個

Employee

正是

AddEmployeeDTO.Employee

。這個時候可能會困擾了,我明明就是傳入

AddEmployeeDTO

,而且類裡面壓根就沒有

Map

,為啥會抛出

java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee

這個異常呢?

讓我們

Debug

一下看看發生了啥。

Beanutils造成dubbo反序列化失敗?

  這個時候你會發現接收到的

AddEmployeeDTO.employees

記憶體儲的并不是一個

AddEmployeeDTO$Employee

對象,而是一個

HashMap

。那看來真相大白了,原來是dubbo反序列化的時候将

AddEmployeeDTO$Employee

HashMap

了。進而導緻了

java.util.HashMap cannot be cast to com.aixiao.inv.common.dto.tax.AddEmployeeDTO$Employee

異常的抛出。

Beanutils造成dubbo反序列化失敗?

你以為結束了?

  為啥

Dubbo

反序列化時會将

AddEmployeeDTO$Employee

變成

Map

呢?我們回過頭看看之前列印參數的日志,有一個警告日志提示了

java.lang.ClassNotFoundException:com.aixiao.inv.api.model.form.AddEmployeeForm$Employee

,找不到

AddEmployeeForm$Employee

這個就有點奇怪了,為啥不是

AddEmployeeDTO$Employee

Beanutils造成dubbo反序列化失敗?
Beanutils造成dubbo反序列化失敗?

  在進行

dubbo

調用前

AddEmployeeForm

fromToDTO()

方法将其轉化為

AddEmployeeDTO

。那麼問題會不會出現在這裡呢?我們繼續

Debug

看看。

Beanutils造成dubbo反序列化失敗?

  嘔吼,這下石錘了。原來是在

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繼續進行拷貝。但是涉及到集合我們可以這樣處理:

  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。

參考

結尾

  我是不一樣的科技宅,每天進步一點點,體驗不一樣的生活。我們下期見!

  如果覺得對你有幫助,可以多多評論,多多點贊哦,也可以到我的首頁看看,說不定有你喜歡的文章,也可以随手點個關注哦,謝謝。

繼續閱讀