丟棄掉那些BeanUtils工具類吧,MapStruct真香!!!

△Hollis, 一個對Coding有着獨特追求的人△
這是Hollis的第 298 篇原創分享
做者 l Hollis
來源 l Hollis(ID:hollischuang)

在前幾天的文章《爲何阿里巴巴禁止使用Apache Beanutils進行屬性的copy?》中,我曾經對幾款屬性拷貝的工具類進行了對比。
前端

而後在評論區有些讀者反饋說MapStruct纔是真的香,因而我就抽時間瞭解了一下MapStruct。結果我發現,這真的是一個神仙框架,炒雞香。java

這一篇文章就來簡單介紹下MapStruct的用法,而且再和其餘幾個工具類進行一下對比。git




爲何須要MapStruct ?

首先,咱們先說一下MapStruct這類框架適用於什麼樣的場景,爲何市面上會有這麼多的相似的框架。github

在軟件體系架構設計中,分層式結構是最多見,也是最重要的一種結構。不少人都對三層架構、四層架構等並不陌生。web

甚至有人說:"計算機科學領域的任何問題均可以經過增長一個間接的中間層來解決,若是不行,那就加兩層。"數據庫

可是,隨着軟件架構分層愈來愈多,那麼各個層次之間的數據模型就要面臨着相互轉換的問題,典型的就是咱們能夠在代碼中見到各類O,如DO、DTO、VO等。express

通常狀況下,一樣一個數據模型,咱們在不一樣的層次要使用不一樣的數據模型。如在數據存儲層,咱們使用DO來抽象一個業務實體;在業務邏輯層,咱們使用DTO來表示數據傳輸對象;到了展現層,咱們又把對象封裝成VO來與前端進行交互。apache

那麼,數據的從前端透傳到數據持久化層(從持久層透傳到前端),就須要進行對象之間的互相轉化,即在不一樣的對象模型之間進行映射。編程

一般咱們可使用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依賴方式以下:

    
...

<properties>

    <org.mapstruct.version>1.3.1.Final</org.mapstruct.version>

</properties>

...

<dependencies>

    <dependency>

        <groupId>org.mapstruct</groupId>

        <artifactId>mapstruct</artifactId>

        <version>${org.mapstruct.version}</version>

    </dependency>

</dependencies>

...

<build>

    <plugins>

        <plugin>

            <groupId>org.apache.maven.plugins</groupId>

            <artifactId>maven-compiler-plugin</artifactId>

            <version>3.8.1</version>

            <configuration>

                <source>1.8</source> <!-- depending on your project -->

                <target>1.8</target> <!-- depending on your project -->

                <annotationProcessorPaths>

                    <path>

                        <groupId>org.mapstruct</groupId>

                        <artifactId>mapstruct-processor</artifactId>

                        <version>${org.mapstruct.version}</version>

                    </path>

                    <!-- other annotation processors -->

                </annotationProcessorPaths>

            </configuration>

        </plugin>

    </plugins>

</build>

由於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?》中的示例,咱們對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,他的用法比較簡單,而且功能很是完善,能夠應付各類狀況的字段映射。
而且由於他是編譯期就會生成真正的映射代碼,使得運行期的性能獲得了大大的提高。

強烈推薦,真的很香!!!


    

往期推薦

用了Dapper以後通篇仍是SqlConnection,真的看不下去了


實用!一鍵生成數據庫文檔,堪稱數據庫界的Swagger


爲何阿里巴巴要求日期格式化時必須有使用y表示年,而不能用Y?


本文由「壹伴編輯器」提供技術支
 

直面Java第329期:哪一個命令能夠監控虛擬機各類運行狀態信息?

深刻併發第013期:拓展synchronized——鎖優化


若是你喜歡本文,

請長按二維碼,關注 Hollis.

轉發至朋友圈,是對我最大的支持。


點個 在看 
喜歡是一種感受
在看是一種支持
↘↘↘

本文分享自微信公衆號 - Hollis(hollischuang)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索