使用BeanUtils.copyProperties踩坑經歷

申城異鄉人發表於2021-05-20

1. 原始轉換

提起物件轉換,每個程式設計師都不陌生,比如專案中經常涉及到的DO、DTO、VO之間的轉換,舉個例子,假設現在有個OrderDTO,定義如下所示:

public class OrderDTO {
    private long id;

    private Long userId;

    private String orderNo;

    private Date gmtCreated;

    // 省略get、set方法
}

有個OrderVO,定義如下所示:

public class OrderVO {
    private long id;

    private long userId;

    private String orderNo;

    private Date gmtCreated;
  
  	// 省略get、set方法
}

如果不使用任何轉換工具,程式碼是下面這樣的:

public static void main(String[] args) {
    OrderDTO orderDTO = new OrderDTO();
    orderDTO.setId(1L);
    orderDTO.setUserId(123L);
    orderDTO.setOrderNo("20210518000001");
    orderDTO.setGmtCreated(new Date());

    OrderVO orderVO = new OrderVO();
    orderVO.setId(orderDTO.getId());
    orderVO.setUserId(orderDTO.getUserId());
    orderVO.setOrderNo(orderDTO.getOrderNo());
    orderVO.setGmtCreated(orderDTO.getGmtCreated());

    System.out.println(orderVO.getId());
    System.out.println(orderVO.getUserId());
    System.out.println(orderVO.getOrderNo());
    System.out.println(orderVO.getGmtCreated());
}

執行結果:

2. 使用BeanUtils.copyProperties轉換

因為專案中類似上面的轉換多而繁瑣,所以很多公司的專案中會使用Spring框架裡的BeanUtils.copyProperties來做物件轉換,程式碼如下所示:

OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orderDTO, orderVO);

一行程式碼搞定,很方便,執行結果也和原來一模一樣。

不過這個工具帶來便利的同時,也帶來了很多問題,稍微不注意就會踩坑,接下來就總結下使用這個工具常見的幾個坑。

3. 踩坑經歷

3.1 包裝型別轉基本型別問題

java.lang.IllegalArgumentException

細心的你可能會發現,OrderDTO中的userId欄位,我定義的是Long型別:

而OrderVO中的userId欄位,我定義的是long型別:

然後我們執行下下面所示的程式碼:

public static void main(String[] args) {
    OrderDTO orderDTO = new OrderDTO();
    orderDTO.setId(1L);
    orderDTO.setUserId(null);
    orderDTO.setOrderNo("20210518000001");
    orderDTO.setGmtCreated(new Date());

    OrderVO orderVO = new OrderVO();
    BeanUtils.copyProperties(orderDTO, orderVO);
}

會看到程式碼拋了java.lang.IllegalArgumentException異常:

3.2 空格問題

假設OrderVO的orderNo欄位,是使用者自定義的,使用者不小心輸入了空格,使用BeanUtils.copyProperties後,空格會帶入到OrderDTO的orderNo欄位,如果不小心,就會把髒資料落到資料庫(而我們希望的是去除空格再落庫的),造成一系列後續問題:

public static void main(String[] args) {
    OrderVO orderVO = new OrderVO();
    orderVO.setId(1L);
    orderVO.setUserId(123L);
    // 模擬空格場景
    orderVO.setOrderNo(" 20210518000001 ");
    orderVO.setGmtCreated(new Date());

    OrderDTO orderDTO = new OrderDTO();
    BeanUtils.copyProperties(orderVO, orderDTO);

    System.out.println(orderDTO.getOrderNo());
}

執行結果:

3.3 查詢不到欄位引用

使用BeanUtils.copyProperties後,會看到欄位並沒有引用,其實是有用到的,如下圖所示:

有些小夥伴在看程式碼時,看到欄位沒有地方引用,可能就忍不住想刪掉,結果就導致真正使用該欄位的地方取不到值,產生bug。

3.4 前端誤傳欄位,直接把資料庫覆蓋了

如果介面定義的比較嚴謹,理論上是不應該存在這種情況的,不過凡事總有特殊,這裡舉個介面不嚴謹導致資料被覆蓋的例子。

假如OrderVO和OrderDTO有如下2個欄位:

/**
 * 已收金額
 * 單位:分
 */
private Long receivedAmount;

/**
 * 備註
 */
private String remark;

正常情況下,後端只應該使用前端傳遞的remark欄位,receivedAmount欄位不應該使用,但假如使用者修改訂單備註時,前端不小心傳遞了receivedAmount欄位,並且賦值為null,這時使用BeanUtils.copyProperties後,OrderDTO裡的receivedAmount欄位就也為null,如果後端不知道前端傳遞了這個欄位並且操作DB不夠嚴謹,就會導致訂單的已收金額被清空,很恐怖,而且不好排查原因。

4. 外掛推薦

雖然BeanUtils.copyProperties工具提供了便利,但帶來的問題也很多,因此很多公司(包含我現在所在的公司)都禁止在專案中使用該工具。

但重複的寫物件轉換,實在是太繁瑣,效率太低了,這裡推薦一個IDEA的外掛GenerateAllSetter,可以一鍵生成物件的set方法,非常方便,如下圖所示:

外掛使用:

在需要生成set方法的物件上,按快捷鍵Option+Enter(Windows是Alt+Enter),會看到下圖所示的選項:

點選後會自動生成所有欄位(沒有預設值)的賦值語句:

如果生成賦值語句時想帶預設值,可以使用另一個選項:

效果如下所示:

相關文章