【原創】如何優雅的轉換Bean物件

孤獨煙發表於2020-09-07

背景

我們的故事要從一個風和日麗的下午開始說起!

這天,外包韓在位置上寫程式碼~外包韓根據如下定義

  • PO(persistant object):持久化物件,可以看成是與資料庫中的表相對映的 java 物件。最簡單的 PO 就是對應資料庫中某個表中的一條記錄。
  • VO(view object):檢視物件,用於展示層,它的作用是把某個指定頁面(或元件)的所有資料封裝起來。
  • BO(business object):業務物件,主要作用是把業務邏輯封裝為一個物件。這個物件可以包括一個或多個其它的物件。
  • DTO、DO(省略......)

將Bean進行逐一分類!例如一個car_tb的表,於是他有了兩個類,一個叫CarPo,裡頭屬性和表欄位完全一致。另一個叫CarVo,用於頁面上的Car顯示!
但是外包韓在做CarPo到CarVo轉換的時候,程式碼是這麼寫的,虛擬碼如下:

CarPo carPo = this.carDao.selectById(1L);
CarVo carVo = new CarVo();
carVo.setId(carPo.getId());
carVo.setName(carPo.getName());
//省略一堆
return carVo;

畫外音:看到這一串程式碼是不是特別親切,我接手過一堆外包留下的程式碼,就是這麼寫的,一坨屎山!一類幾千行,一半都在set屬性。

恰巧,阿雄打水路過!雞賊的阿雄瞄了一眼外包韓的螢幕,看到外包韓的這一系列程式碼!上去進行一頓教育,覺得不夠優雅!阿雄覺得,應該用BeanUtils.copyProperties來簡化書寫,像下面這樣!

CarPo carPo = this.carDao.selectById(1L);
CarVo carVo = new CarVo();
BeanUtils.copyProperties(carPo, carVo);
return carVo;

可是,外包韓盯著這段程式碼,說道:"網上不是說反射效率慢,你這麼寫,沒有效能問題麼?"
阿雄說道:" 如果是用Apache的BeanUtil類,確實有很大的效能問題,像阿里巴巴的程式碼掃描外掛,都禁止用該類,如下所示!"

"但是,如果採用的是像Spring的BeanUtils類,要在呼叫次數足夠多的時候,你才能明顯的感受到卡頓。"阿雄補充道。

"哇,阿雄真棒!"外包韓興奮不已!

看著這辦公室基情滿滿的氛圍。一旁正在拖地的清潔工------掃地煙,他決定不再沉默。

只見掃地煙扔掉手中的拖把,得瑟的說道"我們不考慮效能。從擴充性角度看看!BeanUtils還是有很多問題的!"

  • 複製物件時欄位型別不一致,導致賦值不上,你怎麼解決?自己擴充?
  • 複製物件時欄位名稱不一致,例如CarPo裡叫carName,CarVo裡叫name,導致賦值不上,你怎麼解決?自己擴充?
  • 如果是集合類的複製,例如List轉換為List,你怎麼處理?
    (省略一萬字....)

"那應該怎麼辦呢?"聽了掃地煙的描述,外包韓疑惑的問道!

"很簡單,其實我們在轉換bean的過程中,set這些邏輯是固定的,唯一變化的就是轉換規則。因此,如果我們只需要書寫轉換規則,轉換程式碼由系統根據規則自動生成,就方便很多了!還是用上面的例子,CarPo裡叫carName,CarVo裡叫name,屬性名稱不一致。我們就通過一個註解

@Mapping(source = "carName", target = "name"),

指定對應轉換規則。系統識別到這個註解,就會生成程式碼

carVo.setName(carPo.getCarName())

如果能以這樣的方式,set程式碼由系統自動生成,那麼在bean轉換邏輯方面,靈活性將大大加強,而且這種方式不存在效能問題!"掃地煙補充道!

"那這些set邏輯,由什麼工具來生成呢?"外包韓和阿雄一起問道!

"工具的名字叫MapStruct!"

ok,上面的故事到了這裡,就結束了!不需要問結局,結局只有一個,外包韓和阿雄幸福美滿的...(省略10000字)...
那麼我們開始具體來說一說MapStruct

MapStruct的教程

這裡從用法、原理、優勢三個角度來介紹一下這個外掛,至於詳細教程,還是看官方文件吧。

用法

引入pom檔案如下

<dependency>
    <groupId>org.mapstruct</groupId>
    <!-- jdk8以下就使用mapstruct -->
    <artifactId>mapstruct-jdk8</artifactId>
    <version>1.2.0.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.2.0.Final</version>
</dependency>

在準備兩個實體類,為了方便演示,用了lombok外掛。
準備兩個實體類,一個是CarPo

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarPo {
    private Integer id;
    private String brand;
}

還有一個是CarVo

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarVo {
    private Integer id;
    private String brand;
}

再來一個轉換介面

@Mapper
public interface CarCovertBasic {
    CarCovertBasic INSTANCE = 
    Mappers.getMapper(CarCovertBasic.class);
    
    CarVo toConvertVo(CarPo source);
}

測試程式碼如下:

//實際中從資料庫取
CarPo carPo = CarPo.builder().id(1)
                           .brand("BMW")
                           .build();
CarVo carVo = CarCovertBasic.INSTANCE.toConvertVo(carPo);
System.out.println(carVo);

輸出如下

CarVo(id=1, brand=BMW)

可以看到,carPo的屬性值複製給了carVo。當然,在這種情況下,功能和BeanUtils是差不多的,體現不出優勢!嗯,我們放在後面說,我們先來說說原理!

原理

其實原理就是MapStruct外掛會識別我們的介面,生成一個實現類,在實現類中,為我們實現了set邏輯!
例如,上面的例子中,給CarCovertBasic介面,實現了一個實現類CarCovertBasicImpl,我們可以用反編譯工具看到原始碼如下圖所示

下面,我們來說說優勢

優勢

(1)兩個型別屬性不一致
此時CarPo的一個屬性為carName,而CarVo對應的屬性為name!

我們在介面上增加對應關係即可,如下所示

@Mapper
public interface CarCovertBasic {
CarCovertBasic INSTANCE = Mappers.getMapper(CarCovertBasic.class);

@Mapping(source = "carName", target = "name")
CarVo toConvertVo(CarPo source);
}

測試程式碼如下

CarPo carPo = CarPo.builder().id(1)
                       .brand("BMW")
                       .carName("寶馬")
                       .build();
CarVo carVo = CarCovertBasic.INSTANCE.toConvertVo(carPo);
System.out.println(carVo);

輸出如下

CarVo(id=1, brand=BMW, name=寶馬)

可以看到carVo已經能識別到carPo中的carName屬性,並賦值成功。反編譯的圖如下

畫外音:如果有多個對映關係可以用@Mappings註解,巢狀多個@Mapping註解實現,後文說明!

(2)集合型別轉換
如果我們要從List轉換為List怎麼辦呢?
簡單,介面裡加一個方法就行

@Mapper
public interface CarCovertBasic {
    CarCovertBasic INSTANCE = Mappers.getMapper(CarCovertBasic.class);

    @Mapping(source = "carName", target = "name")
    CarVo toConvertVo(CarPo source);

    List<CarVo> toConvertVos(List<CarPo> source);
}

如程式碼所示,我們增加了一個toConvertVos方法即可,mapStruct生成程式碼的時候,會幫我們去迴圈呼叫toConvertVo方法,給大家看一下反編譯的程式碼,就一目瞭然

(3)型別不一致
在CarPo加一個屬性為Date型別的createTime,而在CarVo加一個屬性為String型別的createTime,那麼程式碼如下

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarPo {
    private Integer id;
    private String brand;
    private String carName;
    private Date createTime;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarVo {
    private Integer id;
    private String brand;
    private String name;
    private String createTime;
}

介面就可以這麼寫

@Mapper
public interface CarCovertBasic {
    CarCovertBasic INSTANCE = Mappers.getMapper(CarCovertBasic.class);
    @Mappings({
        @Mapping(source = "carName", target = "name"),
        @Mapping(target = "createTime", expression = "java(com.guduyan.util.DateUtil.dateToStr(source.getCreateTime()))")
    })
    CarVo toConvertVo(CarPo source);

    List<CarVo> toConvertVos(List<CarPo> source);
}

這樣在程式碼中,就能解決型別不一致的問題!在生成set方法的時候,自動呼叫DateUtil類進行轉換,由於比較簡單,我就不貼反編譯的圖了!

(4)多對一
在實際業務情況中,我們有時候會遇到將兩個Bean對映為一個Bean的情況,假設我們此時還有一個類為AtrributePo,我們要將CarPo和AttributePo同時對映為CarBo,我們可以這麼寫

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AttributePo {
    private double price;
    private String color;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarBo {
    private Integer id;
    private String brand;
    private String carName;
    private Date createTime;
    private double price;
    private String color;
}

介面改變如下

@Mapper
public interface CarCovertBasic {
    CarCovertBasic INSTANCE = Mappers.getMapper(CarCovertBasic.class);
    @Mappings({
        @Mapping(source = "carName", target = "name"),
        @Mapping(target = "createTime", expression = "java(com.guduyan.util.DateUtil.dateToStr(source.getCreateTime()))")
    })
    CarVo toConvertVo(CarPo source);

    List<CarVo> toConvertVos(List<CarPo> source);

    CarBo toConvertBo(CarPo source1, AttributePo source2);
}

直接增加介面即可,外掛在生成程式碼的時候,會幫我們自動組裝,看看下面的反編譯程式碼就一目瞭然。

(5)其他
關於MapStruct還有其他很多的高階功能,我就不一一介紹了。大家可以參考下面的文件,在用到的時候自行翻閱即可!
文件地址:https://mapstruct.org/documentation/reference-guide/

總結

本文介紹了,在專案裡如何優雅的轉換Bean,希望大家有所收穫!
還想聽到其他關於阿雄的故事麼,請記得關注"孤獨煙!"

相關文章