丟棄掉那些BeanUtils工具類吧,MapStruct真香!!!
阿新 • • 發佈:2020-08-10
在前幾天的文章《[為什麼阿里巴巴禁止使用Apache Beanutils進行屬性的copy?][1]》中,我曾經對幾款屬性拷貝的工具類進行了對比。
然後在評論區有些讀者反饋說MapStruct才是真的香,於是我就抽時間瞭解了一下MapStruct。結果我發現,這真的是一個神仙框架,炒雞香。
這一篇文章就來簡單介紹下MapStruct的用法,並且再和其他幾個工具類進行一下對比。
### 為什麼需要MapStruct ?
首先,我們先說一下MapStruct這類框架適用於什麼樣的場景,為什麼市面上會有這麼多的類似的框架。
在軟體體系架構設計中,分層式結構是最常見,也是最重要的一種結構。很多人都對三層架構、四層架構等並不陌生。
甚至有人說:**"電腦科學領域的任何問題都可以通過增加一個間接的中間層來解決,如果不行,那就加兩層。"**
但是,隨著軟體架構分層越來越多,那麼各個層次之間的資料模型就要面臨著相互轉換的問題,典型的就是我們可以在程式碼中見到各種O,如DO、DTO、VO等。
一般情況下,同樣一個數據模型,我們在不同的層次要使用不同的資料模型。**如在資料儲存層,我們使用DO來抽象一個業務實體;在業務邏輯層,我們使用DTO來表示資料傳輸物件;到了展示層,我們又把物件封裝成VO來與前端進行互動。**
那麼,資料的從前端透傳到資料持久化層(從持久層透傳到前端),就需要進行物件之間的互相轉化,即在不同的物件模型之間進行對映。
通常我們可以使用get/set等方式逐一進行欄位對映操作,如:
personDTO.setName(personDO.getName());
personDTO.setAge(personDO.getAge());
personDTO.setSex(personDO.getSex());
personDTO.setBirthday(personDO.getBirthday());
但是,編寫這樣的對映程式碼是一項冗長且容易出錯的任務。MapStruct等類似的框架的目標是通過自動化的方式儘可能多地簡化這項工作。
### MapStruct的使用
MapStruct(https://mapstruct.org/ )是一種程式碼生成器,它極大地簡化了基於"約定優於配置"方法的Java bean型別之間對映的實現。生成的對映程式碼使用純方法呼叫,因此快速、型別安全且易於理解。
> 約定優於配置,也稱作按約定程式設計,是一種軟體設計正規化,旨在減少軟體開發人員需做決定的數量,獲得簡單的好處,而又不失靈活性。
假設我們有兩個類需要進行互相轉換,分別是PersonDO和PersonDTO,類定義如下:
public class PersonDO {
private Integer id;
private String name;
private int age;
private Date birthday;
private String gender;
}
public class PersonDTO {
private String userName;
private Integer age;
private Date birthday;
private Gender gender;
}
我們演示下如何使用MapStruct進行bean對映。
想要使用MapStruct,首先需要依賴他的相關的jar包,使用maven依賴方式如下:
...
...
...
因為MapStruct需要在編譯器生成轉換程式碼,所以需要在maven-compiler-plugin外掛中配置上對mapstruct-processor的引用。這部分在後文會再次介紹。
之後,我們需要定義一個做對映的介面,主要程式碼如下:
@Mapper
interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
@Mappings(@Mapping(source = "name", target = "userName"))
PersonDTO do2dto(PersonDO person);
}
使用註解 `@Mapper`定義一個Converter介面,在其中定義一個do2dto方法,方法的入參型別是PersonDO,出參型別是PersonDTO,這個方法就用於將PersonDO轉成PersonDTO。
測試程式碼如下:
public static void main(String[] args) {
PersonDO personDO = new PersonDO();
personDO.setName("Hollis");
personDO.setAge(26);
personDO.setBirthday(new Date());
personDO.setId(1);
personDO.setGender(Gender.MALE.name());
PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);
System.out.println(personDTO);
}
輸出結果:
PersonDTO{userName='Hollis', age=26, birthday=Sat Aug 08 19:00:44 CST 2020, gender=MALE}
可以看到,我們使用MapStruct完美的將PersonDO轉成了PersonDTO。
上面的程式碼可以看出,MapStruct的用法比較簡單,主要依賴`@Mapper`註解。
但是我們知道,大多數情況下,我們需要互相轉換的兩個類之間的屬性名稱、型別等並不完全一致,還有些情況我們並不想直接做對映,那麼該如何處理呢?
其實MapStruct在這方面也是做的很好的。
### MapStruct處理欄位對映
首先,可以明確的告訴大家,如果要轉換的兩個類中源物件屬性與目標物件屬性的型別和名字一致的時候,會自動對映對應屬性。
那麼,如果遇到特殊情況如何處理呢?
#### 名字不一致如何對映
如上面的例子中,在PersonDO中用name表示使用者名稱稱,而在PersonDTO中使用userName表示使用者名稱,那麼如何進行引數對映呢。
這時候就要使用`@Mapping`註解了,只需要在方法簽名上,使用該註解,並指明需要轉換的源物件的名字和目標物件的名字就可以了,如將name的值對映給userName,可以使用如下方式:
@Mapping(source = "name", target = "userName")
#### 可以自動對映的型別
除了名字不一致以外,還有一種特殊情況,那就是型別不一致,如上面的例子中,在PersonDO中用String型別表示使用者性別,而在PersonDTO中使用一個Genter的列舉表示使用者性別。
這時候型別不一致,就需要涉及到互相轉換的問題
其實,MapStruct會對部分型別自動做對映,不需要我們做額外配置,如例子中我們將String型別自動轉成了列舉型別。
一般情況下,對於以下情況可以做自動型別轉換:
* 基本型別及其他們對應的包裝型別。
* 基本型別的包裝型別和String型別之間
* String型別和列舉型別之間
#### 自定義常量
如果我們在轉換對映過程中,想要給一些屬性定義一個固定的值,這個時候可以使用 constant
@Mapping(source = "name", constant = "hollis")
#### 型別不一致的如何對映
還是上面的例子,如果我們需要在Person這個物件中增加家庭住址這個屬性,那麼我們一般在PersonoDTO中會單獨定義一個HomeAddress類來表示家庭住址,而在Person類中,我們一般使用String型別表示家庭住址。
這就需要在HomeAddress和String之間使用JSON進行互相轉化,這種情況下,MapStruct也是可以支援的。
public class PersonDO {
private String name;
private String address;
}
public class PersonDTO {
private String userName;
private HomeAddress address;
}
@Mapper
interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
@Mapping(source = "userName", target = "name")
@Mapping(target = "address",expression = "java(homeAddressToString(dto2do.getAddress()))")
PersonDO dto2do(PersonDTO dto2do);
default String homeAddressToString(HomeAddress address){
return JSON.toJSONString(address);
}
}
我們只需要在PersonConverter中在定義一個方法(因為PersonConverter是一個介面,所以在JDK 1.8以後的版本中可以定義一個default方法),這個方法的作用就是將HomeAddress轉換成String型別。
> default方法:Java 8 引入的新的語言特性,用關鍵字default來標註,被default所標註的方法,需要提供實現,而子類可以選擇實現或者不實現該方法
然後在dto2do方法上,通過以下註解方式即可實現型別的轉換:
@Mapping(target = "address",expression = "java(homeAddressToString(dto2do.getAddress()))")
上面這種是自定義的型別轉換,還有一些型別的轉換是MapStruct本身就支援的,如String和Date之間的轉換:
@Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")
以上,簡單介紹了一些常用的欄位對映的方法,也是我自己在工作中經常遇到的幾個場景,更多的情況大家可以檢視官方的示例(https://github.com/mapstruct/mapstruct-examples)。
### MapStruct的效能
前面說了這麼多MapStruct的用法,可以看出MapStruct的使用還是比較簡單的,並且欄位對映上面的功能很強大,那麼他的效能到底怎麼樣呢?
參考《[為什麼阿里巴巴禁止使用Apache Beanutils進行屬性的copy?][1]》中的示例,我們對MapStruct進行效能測試。
分別執行1000、10000、100000、1000000次對映的耗時分別為:0ms、1ms、3ms、6ms。
可以看到,**MapStruct的耗時相比較於其他幾款工具來說是非常短的**。
那麼,為什麼MapStruct的效能可以這麼好呢?
其實,MapStruct和其他幾類框架最大的區別就是:**與其他對映框架相比,MapStruct在編譯時生成bean對映,這確保了高效能,可以提前將問題反饋出來,也使得開發人員可以徹底的錯誤檢查。**
還記得前面我們在引入MapStruct的依賴的時候,特別在maven-compiler-plugin中增加了mapstruct-processor的支援嗎?
並且我們在程式碼中使用了很多MapStruct提供的註解,這使得在編譯期,MapStruct就可以直接生成bean對映的程式碼,相當於代替我們寫了很多setter和getter。
如我們在程式碼中定義了以下一個Mapper:
@Mapper
interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
@Mapping(source = "userName", target = "name")
@Mapping(target = "address",expression = "java(homeAddressToString(dto2do.getAddress()))")
@Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")
PersonDO dto2do(PersonDTO dto2do);
default String homeAddressToString(HomeAddress address){
return JSON.toJSONString(address);
}
}
經過程式碼編譯後,會自動生成一個PersonConverterImpl:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2020-08-09T12:58:41+0800",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
)
class PersonConverterImpl implements PersonConverter {
@Override
public PersonDO dto2do(PersonDTO dto2do) {
if ( dto2do == null ) {
return null;
}
PersonDO personDO = new PersonDO();
personDO.setName( dto2do.getUserName() );
if ( dto2do.getAge() != null ) {
personDO.setAge( dto2do.getAge() );
}
if ( dto2do.getGender() != null ) {
personDO.setGender( dto2do.getGender().name() );
}
personDO.setAddress( homeAddressToString(dto2do.getAddress()) );
return personDO;
}
}
在執行期,對於bean進行對映的時候,就會直接呼叫PersonConverterImpl的dto2do方法,這樣就沒有什麼特殊的事情要做了,只是在記憶體中進行set和get就可以了。
所以,因為在編譯期做了很多事情,所以MapStruct在執行期的效能會很好,並且還有一個好處,那就是可以把問題的暴露提前到編譯期。
使得如果程式碼中欄位對映有問題,那麼應用就會無法編譯,強制開發者要解決這個問題才行。
### 總結
本文介紹了一款Java中的欄位對映工具類,MapStruct,他的用法比較簡單,並且功能非常完善,可以應付各種情況的欄位對映。
並且因為他是編譯期就會生成真正的對映程式碼,使得執行期的效能得到了大大的提升。
強烈推薦,真的很香!!!
[1]: https://www.hollischuang.com/archives/5337