【原創】如何優雅的轉換Bean對象

背景

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

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

  • PO(persistant object):持久化對象,可以看成是與數據庫中的表相映射的 java 對象。最簡單的 PO 就是對應數據庫中某個表中的一條記錄。
  • VO(view object):視圖對象,用於展示層,它的作用是把某個指定頁面(或組件)的所有數據封裝起來。
  • BO(view 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還有其他很多的高級功能,我就不一一介紹了。大家可以參考下面的文檔,在用到的時候自行翻閱即可!
文檔地址://mapstruct.org/documentation/reference-guide/

總結

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