天天看點

丢棄 BeanUtils!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 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 address;

    private HomeAddress address;

    @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:

    @Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")

}經過代碼編譯後,會自動生成一個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,他的用法比較簡單,并且功能非常完善,可以應付各種情況的字段映射。

并且因為他是編譯期就會生成真正的映射代碼,使得運作期的性能得到了大大的提升。

https://mp.weixin.qq.com/s/gSGfnfhb2f2CI6bFjOiNOw