刪除掉各種BeanUtils工具類吧,MapStruct是真的很可以的,不信你試試??!

?????在前幾天的文章《為什么阿里巴巴禁止使用Apache Beanutils進(jìn)行屬性的copy?》中,我曾經(jīng)對幾款屬性拷貝的工具類進(jìn)行了對比。然后在評論區(qū)有些讀者反饋說MapStruct才是真的香,于是我就抽時間了解了一下MapStruct。結(jié)果我發(fā)現(xiàn),這真的是一個神仙框架,炒雞香。
這一篇文章就來簡單介紹下MapStruct的用法,并且再和其他幾個工具類進(jìn)行一下對比。

一、為什么需要MapStruct ?

首先,我們先說一下MapStruct這類框架適用于什么樣的場景,為什么市面上會有這么多的類似的框架。
在軟件體系架構(gòu)設(shè)計中,分層式結(jié)構(gòu)是最常見,也是最重要的一種結(jié)構(gòu)。很多人都對三層架構(gòu)、四層架構(gòu)等并不陌生。
甚至有人說:"計算機(jī)科學(xué)領(lǐng)域的任何問題都可以通過增加一個間接的中間層來解決,如果不行,那就加兩層。"
但是,隨著軟件架構(gòu)分層越來越多,那么各個層次之間的數(shù)據(jù)模型就要面臨著相互轉(zhuǎn)換的問題,典型的就是我們可以在代碼中見到各種O,如DO、DTO、VO等。

一般情況下,同樣一個數(shù)據(jù)模型,我們在不同的層次要使用不同的數(shù)據(jù)模型。如在數(shù)據(jù)存儲層,我們使用DO來抽象一個業(yè)務(wù)實體;在業(yè)務(wù)邏輯層,我們使用DTO來表示數(shù)據(jù)傳輸對象;到了展示層,我們又把對象封裝成VO來與前端進(jìn)行交互。

那么,數(shù)據(jù)的從前端透傳到數(shù)據(jù)持久化層(從持久層透傳到前端),就需要進(jìn)行對象之間的互相轉(zhuǎn)化,即在不同的對象模型之間進(jìn)行映射。

通常我們可以使用get/set等方式逐一進(jìn)行字段映射操作,很是麻煩,所以就是可以使用對象的拷貝工具類來代替這個繁瑣的代碼。

二、MapStruct的使用

MapStruct(https://mapstruct.org/ )是一種代碼生成器,它極大地簡化了基于"約定優(yōu)于配置"方法的Java bean類型之間映射的實現(xiàn)。生成的映射代碼使用純方法調(diào)用,因此快速、類型安全且易于理解。

約定優(yōu)于配置,也稱作按約定編程,是一種軟件設(shè)計范式,旨在減少軟件開發(fā)人員需做決定的數(shù)量,獲得簡單的好處,而又不失靈活性。

假設(shè)我們有兩個類需要進(jìn)行互相轉(zhuǎn)換,分別是PersonDO和PersonDTO,類定義如下:

@Data
public class PersonDO {
    private Integer id;
    private String name;
    private int age;
    private Date birthday;
    private String gender;
}

@Data
public class PersonDTO {
    private String userName;
    private Integer age;
    private Date birthday;
    private Gender gender;
}

我們演示下如何使用MapStruct進(jìn)行bean映射。

想要使用MapStruct,首先需要依賴他的相關(guān)的jar包,使用maven依賴方式如下:

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.3.1.Final</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.3.1.Final</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

之后,我們需要定義一個做映射的接口,主要代碼如下:

/**
 * @author Mr.Jia
 * @description 需要定義一個做映射的接口
 * @date 2020/8/17 16:14
 */
@Mapper
interface PersonConverter {
    PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    @Mappings(@Mapping(source = "name", target = "userName"))
    PersonDTO do2dto(PersonDO person);
}

使用注解@Mapper定義一個Converter接口,在其中定義一個do2dto方法,方法的入?yún)㈩愋褪荘ersonDO,出參類型是PersonDTO,這個方法就用于將PersonDO轉(zhuǎn)成PersonDTO。

測試代碼如下:

/**
 * @Description: 測試轉(zhuǎn)換
 * @Author: Mr.Jia
 * @Date: 2020/8/17 16:14
 */
public class Test {
    public static void main(String[] args) {
        PersonDO personDO = new PersonDO();
        personDO.setName("JIA");
        personDO.setAge(21);
        personDO.setBirthday(new Date());
        personDO.setId(8);
        PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);
        System.out.println(personDTO);
    }
}

輸出結(jié)果:

PersonDTO(userName=JIA, age=21, birthday=Mon Aug 17 16:15:58 CST 2020, gender=null)

可以看到,我們使用MapStruct完美的將PersonDO轉(zhuǎn)成了PersonDTO。上面的代碼可以看出,MapStruct的用法比較簡單,主要依賴@Mapper注解。但是我們知道,大多數(shù)情況下,我們需要互相轉(zhuǎn)換的兩個類之間的屬性名稱、類型等并不完全一致,還有些情況我們并不想直接做映射,那么該如何處理呢?其實MapStruct在這方面也是做的很好的。

三、MapStruct處理字段映射

首先,可以明確的告訴大家,如果要轉(zhuǎn)換的兩個類中源對象屬性與目標(biāo)對象屬性的類型和名字一致的時候,會自動映射對應(yīng)屬性。

那么,如果遇到特殊情況如何處理呢?

  1. 名字不一致如何映射

    如上面的例子中,在PersonDO中用name表示用戶名稱,而在PersonDTO中使用userName表示用戶名,那么如何進(jìn)行參數(shù)映射呢。這時候就要使用@Mapping注解了,只需要在方法簽名上,使用該注解,并指明需要轉(zhuǎn)換的源對象的名字和目標(biāo)對象的名字就可以了,如將name的值映射給userName,可以使用如下方式:

    @Mapping(source = "name", target = "userName")
    
  2. 可以自動映射的類型

    除了名字不一致以外,還有一種特殊情況,那就是類型不一致,如上面的例子中,在PersonDO中用String類型表示用戶性別,而在PersonDTO中使用一個Genter的枚舉表示用戶性別。

    這時候類型不一致,就需要涉及到互相轉(zhuǎn)換的問題

    其實,MapStruct會對部分類型自動做映射,不需要我們做額外配置,如例子中我們將String類型自動轉(zhuǎn)成了枚舉類型。

    一般情況下,對于以下情況可以做自動類型轉(zhuǎn)換:

    • 基本類型及其他們對應(yīng)的包裝類型。
    • 基本類型的包裝類型和String類型之間
    • String類型和枚舉類型之間
  3. 自定義常量

    如果我們在轉(zhuǎn)換映射過程中,想要給一些屬性定義一個固定的值,這個時候可以使用 constant

    @Mapping(source = "name", constant = "JIA")
    
  4. 類型不一致的如何映射

    還是上面的例子,如果我們需要在Person這個對象中增加家庭住址這個屬性,那么我們一般在PersonoDTO中會單獨定義一個HomeAddress類來表示家庭住址,而在Person類中,我們一般使用String類型表示家庭住址。這就需要在HomeAddress和String之間使用JSON進(jìn)行互相轉(zhuǎn)化,這種情況下,MapStruct也是可以支持的。

    @Data
    public class PersonDO {
        private String name;
        private String address;
    }
    
    @Data
    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轉(zhuǎn)換成String類型。

    default方法:Java 8 引入的新的語言特性,用關(guān)鍵字default來標(biāo)注,被default所標(biāo)注的方法,需要提供實現(xiàn),而子類可以選擇實現(xiàn)或者不實現(xiàn)該方法

    然后在dto2do方法上,通過以下注解方式即可實現(xiàn)類型的轉(zhuǎn)換:

    @Mapping(target = "address",expression = "java(homeAddressToString(dto2do.getAddress()))")
    

    上面這種是自定義的類型轉(zhuǎn)換,還有一些類型的轉(zhuǎn)換是MapStruct本身就支持的,如String和Date之間的轉(zhuǎn)換:

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

    以上,簡單介紹了一些常用的字段映射的方法,也是我自己在工作中經(jīng)常遇到的幾個場景,更多的情況大家可以查看官方的示例(https://github.com/mapstruct/mapstruct-examples)。

四、MapStruct的性能

前面說了這么多MapStruct的用法,可以看出MapStruct的使用還是比較簡單的,并且字段映射上面的功能很強(qiáng)大,那么他的性能到底怎么樣呢?

參考《為什么阿里巴巴禁止使用Apache Beanutils進(jìn)行屬性的copy?》中的示例,我們對MapStruct進(jìn)行性能測試。

分別執(zhí)行1000、10000、100000、1000000次映射的耗時分別為:0ms、1ms、3ms、6ms。

可以看到,MapStruct的耗時相比較于其他幾款工具來說是非常短的。

那么,為什么MapStruct的性能可以這么好呢?

其實,MapStruct和其他幾類框架最大的區(qū)別就是:與其他映射框架相比,MapStruct在編譯時生成bean映射,這確保了高性能,可以提前將問題反饋出來,也使得開發(fā)人員可以徹底的錯誤檢查。

引入MapStruct的依賴的時候,并且我們在代碼中使用了很多MapStruct提供的注解,這使得在編譯期,MapStruct就可以直接生成bean映射的代碼,相當(dāng)于代替我們寫了很多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);
    }
}

經(jīng)過代碼編譯后,會自動生成一個PersonConverterImpl:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.mapstruct;
class PersonConverterImpl implements PersonConverter {
    PersonConverterImpl() {
    }
    public PersonDTO do2dto(PersonDO person) {
        if (person == null) {
            return null;
        } else {
            PersonDTO personDTO = new PersonDTO();
            personDTO.setUserName(person.getName());
            personDTO.setAge(person.getAge());
            personDTO.setBirthday(person.getBirthday());
            personDTO.setGender(person.getGender());
            return personDTO;
        }
    }
}

在運(yùn)行期,對于bean進(jìn)行映射的時候,就會直接調(diào)用PersonConverterImpl的dto2do方法,這樣就沒有什么特殊的事情要做了,只是在內(nèi)存中進(jìn)行set和get就可以了。

所以,因為在編譯期做了很多事情,所以MapStruct在運(yùn)行期的性能會很好,并且還有一個好處,那就是可以把問題的暴露提前到編譯期。

使得如果代碼中字段映射有問題,那么應(yīng)用就會無法編譯,強(qiáng)制開發(fā)者要解決這個問題才行。

五、其他拷貝工具類
  • DozerBeanMapper
  • Orika
  • BeanCopier
  • ModelMapper(Model Mapper 是通過反射的性能不是很好的,只是減少了代碼set get量。性能卻下降了,不建議使用在生產(chǎn)環(huán)境)。
  • Bean拷貝工具類比較
六、總結(jié)

本文介紹了一款Java中的字段映射工具類,MapStruct,他的用法比較簡單,并且功能非常完善,可以應(yīng)付各種情況的字段映射。并且因為他是編譯期就會生成真正的映射代碼,使得運(yùn)行期的性能得到了大大的提升。

強(qiáng)烈推薦,真的很香!??!

本文由博客群發(fā)一文多發(fā)等運(yùn)營工具平臺 OpenWrite 發(fā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容