天天看點

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

作者:儒雅程式員阿鑫

程式員的日常搬磚,一旦涉及到業務功能開發,相信對象轉換的操作一定不會陌生,比如項目中 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());
}
           

運作結果:

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

1. 使用BeanUtils.copyProperties轉換

因為項目中類似上面的轉換多而繁瑣,是以很多公司的項目中會使用Spring架構裡的BeanUtils.copyProperties來做對象轉換,代碼如下所示:

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

一行代碼搞定,很友善,運作結果也和原來一模一樣。

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

2. 踩坑經曆

2.1 包裝類型轉基本類型問題

java.lang.IllegalArgumentException
           

細心的你可能會發現,OrderDTO中的userId字段,我定義的是Long類型:

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

而OrderVO中的userId字段,我定義的是long類型:

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

然後我們運作下下面所示的代碼:

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異常:

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

2.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());
}           

運作結果:

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

2.3 查找不到字段引用

使用BeanUtils.copyProperties後,會看到字段并沒有引用,其實是有用到的,如下圖所示:

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

有些小夥伴在看代碼時,看到字段沒有地方引用,可能就忍不住想删掉,結果就導緻真正使用該字段的地方取不到值,産生bug。

2.4 前端誤傳字段,直接把資料庫覆寫了

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

假如OrderVO和OrderDTO有如下2個字段:

/**
 * 已收金額
 * 機關:分
 */
private Long receivedAmount;

/**
 * 備注
 */
private String remark;
           

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

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

3. 插件推薦

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

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

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

插件使用:

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

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

點選後會自動生成所有字段(沒有預設值)的指派語句:

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

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

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

效果如下所示:

你還在用 BeanUtils.copyProperties 拷貝?小心有坑

4. 更好的選擇

現在 Java 的後端開發,普遍使用的是 Spring 生态,由于 BeanUtils 是 Spring 自帶的工具類,親兒子的出鏡率相對較高,但他并不是效率最高的;如果項目中轉換比較多,并發比較大,建議使用 MapStruct