背景
我們的故事要從一個風和日麗的下午開始說起!
這天,外包韓在位置上寫程式碼~外包韓根據如下定義
- 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
簡單,介面裡加一個方法就行
@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,希望大家有所收穫!
還想聽到其他關於阿雄的故事麼,請記得關注"孤獨煙!"