使用MapStruct進行類對象拷貝

基本用法

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

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

@Data
public class PersonDTO {
    private String name;
    private Integer age;
    private Date birthday;
}

我們演示下如何使用MapStruct進行bean映射。

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

...
<properties>
    <org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
    <lombok.version>1.18.10</lombok.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
    <scope>provided</scope>
</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>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.10</version>
                        </path>  
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

因為MapStruct需要在編譯器生成轉(zhuǎn)換代碼,所以需要在maven-compiler-plugin插件中配置上對mapstruct-processor的引用乔夯。這部分在后文會再次介紹款侵。

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

@Mapper
public interface PersonConverter {

    PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    @Mappings({
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "age", target = "myage")
    })
    PersonDTO do2dto(PersonDO person);
}

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

測試代碼如下:

    @Test
    public void mapConstructTest(){
        PersonDO personDO = new PersonDO();
        personDO.setId(1);
        personDO.setName("jim");
        personDO.setAge(29);
        personDO.setBirthday(new Date());
        PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);
        System.out.println(personDTO);

    }

輸出結(jié)果:

PersonDTO(name=jim, age=29, birthday=Sun Aug 16 15:00:56 CST 2020)

假如存在名稱字段不一致的情況需要映射應(yīng)該怎么處理呢儒老?下面通過一個案例加以說明记餐,例如:

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

@Data
public class PersonDTO {
    private String name;
    private Integer myage;
    private Date birthday;
}

改寫Converter類如下:

@Mapper
public interface PersonConverter {

    PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    PersonDTO do2dto(PersonDO person);
}

單元測試:

@Test
    public void mapConstructTest(){
        PersonDO personDO = new PersonDO();
        personDO.setId(1);
        personDO.setUsername("jim");
        personDO.setAge(29);
        personDO.setBirthday(new Date());
        PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);
        System.out.println(personDTO);

    }

運行結(jié)果:

PersonDTO(name=jim, myage=29, birthday=Sun Aug 16 15:05:53 CST 2020)

如果待轉(zhuǎn)換類中存在子類的屬性需要賦值給其他類的屬性應(yīng)該怎么做呢挖腰?

@Data
public class PersonDO {
    private int id;
    private String username;
    private Integer age;
    private Date birthday;
    private UserInfo userInfo;
}

@Data
public class PersonDTO {
    private String name;
    private Integer myage;
    private Date birthday;
    private String img;
}

編寫類型轉(zhuǎn)換類:

@Mapper
public interface PersonConverter {

    PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    @Mappings({
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "age", target = "myage"),
            @Mapping(source="person.userInfo.userImg",target = "img")
    })
    PersonDTO do2dto(PersonDO person);
}

編寫單元測試類:

@Test
    public void mapConstructTest(){
        PersonDO personDO = new PersonDO();
        personDO.setId(1);
        personDO.setUsername("jim");
        personDO.setAge(29);
        personDO.setBirthday(new Date());

        UserInfo userInfo=new UserInfo();
        userInfo.setId(1);
        userInfo.setUserImg("test.png");
        personDO.setUserInfo(userInfo);

        PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);
        System.out.println(personDTO);

    }

運行結(jié)果:

PersonDTO(name=jim, myage=29, birthday=Sun Aug 16 15:13:15 CST 2020, img=test.png)

加入希望在轉(zhuǎn)換的同時對日期格式進行格式化猴仑,PersonDTO中新增了一個formatDate字段用以表示格式化后的日期:

@Data
public class PersonDTO {
    private String name;
    private Integer myage;
    private Date birthday;
    private String img;
    private String formatDate;
}

改寫轉(zhuǎn)換類,

@Mapper
public interface PersonConverter {

    PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    @Mappings({
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "age", target = "myage"),
            @Mapping(source="person.userInfo.userImg",target = "img"),
            @Mapping(source = "birthday", target = "formatDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
    })
    PersonDTO do2dto(PersonDO person);
}

運行單元測試類崖飘,結(jié)果如下:

PersonDTO(name=jim, myage=29, birthday=Sun Aug 16 15:17:15 CST 2020, img=test.png, formatDate=2020-08-16 15:17:15)

加入目標類中有一個屬性language為固定常量值zh坐漏,且被被復(fù)制類中沒有該屬性赊琳,例如:

@Data
public class PersonDO {
    private int id;
    private String username;
    private Integer age;
    private Date birthday;
    private UserInfo userInfo;
}

@Data
public class PersonDTO {
    private String name;
    private Integer myage;
    private String language;
    private Date birthday;
    private String img;
    private String formatDate;
}

撰寫轉(zhuǎn)換類:

@Mapper
public interface PersonConverter {

    PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    @Mappings({
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "age", target = "myage"),
            @Mapping(source="person.userInfo.userImg",target = "img"),
            @Mapping(source = "birthday", target = "formatDate", dateFormat = "yyyy-MM-dd HH:mm:ss"),
            @Mapping(target = "language", constant = "zh")
    })
    PersonDTO do2dto(PersonDO person);
}

運行結(jié)果:

PersonDTO(name=jim, myage=29, language=zh, birthday=Sun Aug 16 15:26:07 CST 2020, img=test.png, formatDate=2020-08-16 15:26:07)

如果我們希望在類屬性進行轉(zhuǎn)換的過程中進行一些更加自定義的操作躏筏,應(yīng)該如何基于mapconstruct的轉(zhuǎn)換類進行擴展呢?切看下面的一個簡單的示例:

新增類HomeAddress:

@Data
public class HomeAddress {
    private String address;
}

PersonDO中新增屬性address

@Data
public class PersonDO {
    private int id;
    private String username;
    private Integer age;
    private Date birthday;
    private UserInfo userInfo;
    private HomeAddress address;
}

PersonDTO中新增屬性address

@Data
public class PersonDTO {
    private String name;
    private Integer myage;
    private String language;
    private Date birthday;
    private String img;
    private String formatDate;
    private String address;
}

修改PersonConverter類如下:

@Mapper
public interface PersonConverter {

    PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    @Mappings({
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "age", target = "myage"),
            @Mapping(source="person.userInfo.userImg",target = "img"),
            @Mapping(source = "birthday", target = "formatDate", dateFormat = "yyyy-MM-dd HH:mm:ss"),
            @Mapping(target = "language", constant = "zh"),
            @Mapping(target="address",expression = "java(homeAddressToString(person.getAddress()))")
    })
    PersonDTO do2dto(PersonDO person);

    default String homeAddressToString(HomeAddress address){
        return JSON.toJSONString(address);
    }
}

單元測試酥泞;

@Mapper
public interface PersonConverter {

    PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    @Mappings({
            @Mapping(source = "username", target = "name"),
            @Mapping(source = "age", target = "myage"),
            @Mapping(source="person.userInfo.userImg",target = "img"),
            @Mapping(source = "birthday", target = "formatDate", dateFormat = "yyyy-MM-dd HH:mm:ss"),
            @Mapping(target = "language", constant = "zh"),
            @Mapping(target="address",expression = "java(homeAddressToString(person.getAddress()))")
    })
    PersonDTO do2dto(PersonDO person);

    default String homeAddressToString(HomeAddress address){
        return JSON.toJSONString(address);
    }
}

運行結(jié)果:

PersonDTO(name=jim, myage=29, language=zh, birthday=Sun Aug 16 15:32:16 CST 2020, img=test.png, formatDate=2020-08-16 15:32:16, address={"address":"test address"})

實現(xiàn)原理

MapStruct和其他幾類框架最大的區(qū)別就是:與其他映射框架相比,MapStruct在編譯時生成bean映射悯姊,這確保了高性能,可以提前將問題反饋出來,也使得開發(fā)人員可以徹底的錯誤檢查辉阶。
還記得前面我們在引入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);
    }
}

經(jīng)過代碼編譯后,會自動生成一個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進行映射的時候唠摹,就會直接調(diào)用PersonConverterImpl的dto2do方法勾拉,這樣就沒有什么特殊的事情要做了藕赞,只是在內(nèi)存中進行set和get就可以了斧蜕。

所以惩激,因為在編譯期做了很多事情,所以MapStruct在運行期的性能會很好风钻,并且還有一個好處骡技,那就是可以把問題的暴露提前到編譯期。

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

學(xué)會查看編譯后的源代碼還是可以幫助我們解決不少問題的富雅,下面以一個筆者在實際使用過程中遇到和解決問題的經(jīng)歷解釋一下如何去查看編譯后的源碼并且借此解決問題的。

首先,像上面提到的進的經(jīng)典用法一下蛤奢,筆者寫了一個Converter的轉(zhuǎn)換類。

    @Mappings({
            @Mapping(source = "page", target = "pageNo"),
            @Mapping(source = "limit", target = "pageSize")
    })
    TaskCarbonQueryParam getNotifyMeInCorpQuery2TaskCarbonQueryParam(GetNotifyMeInCorpQuery getNotifyMeInCorpQuery);

但是在編譯的時候卻產(chǎn)生了如下的報錯:


image.png

由于目標類不是筆者定義的,而是兄弟團隊的小伙伴提供的一個二方API包中定義的bean瓜晤,去看一下這個類的類定義如下:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TaskCarbonQueryParam extends Paginator {

    private String tenantId;

    private String carbonUserId;

   ....
}

@Getter
public abstract class Paginator extends DTO {

    private int pageNo = 1;

    private int pageSize  = 20;

    public void setPageNo(int pageNo) {
        if (pageNo <= 0) {
            pageNo = 1;
        }
        this.pageNo = pageNo;
    }

    public void setPageSize(int pageSize) {
        if (pageSize <= 0) {
            pageSize = 20;
        }
        this.pageSize = pageSize;
    }
}

確認類屬性名稱確實沒有寫錯痢掠,不知道因何原因報錯足画,我們選擇去看一下編譯出來的代碼:

image.png

發(fā)現(xiàn)正逞痛牵可以編譯成功的是直接new一個對象然后往里面挨個set值象缀,但是下面出現(xiàn)問題類央星,因為子類使用了lombok的@Builder注解莉给,但是@Builder注解的副作用在于無法將父類的屬性加入到builder模式中,導(dǎo)致在builder的時候無法取用到父類的屬性造成了失敗徐矩。解決方案是將子類的@builder注解去除掉。

性能對比

Echart折線圖中撰寫echarts腳本:

option = {
    title: {
        text: '拷貝工具類性能對比'
    },
    tooltip: {
        trigger: 'axis'
    },
    legend: {
        data: ['MapStruct', 'Spring BeanUtils', 'Cglib BeanCopier', 'Apache PropertyUtils', 'Apache BeanUtils', 'Dozer']
    },
    grid: {
        left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
    },
    toolbox: {
        feature: {
            saveAsImage: {}
        }
    },
    xAxis: {
        type: 'category',
        boundaryGap: false,
        data: ['1000', '10000', '100000', '1000000']
    },
    yAxis: {
        type: 'value'
    },
    series: [
        {
            name: 'MapStruct',
            type: 'line',
            stack: '總量',
            data: [0, 1, 3, 6]
        },
        {
            name: 'Spring BeanUtils',
            type: 'line',
            stack: '總量',
            data: [5,10,45,169]
        },
        {
            name: 'Cglib BeanCopier',
            type: 'line',
            stack: '總量',
            data: [4,18,45,91]
        },
        {
            name: 'Apache PropertyUtils',
            type: 'line',
            stack: '總量',
            data: [60,265,1444,11492]
        },
        {
            name: 'Apache BeanUtils',
            type: 'line',
            stack: '總量',
            data: [138,816,4154,36938]
        },
        {
            name: 'Dozer',
            type: 'line',
            stack: '總量',
            data: [566, 2254, 11136, 102965]
        }
    
    ]
};
image.png

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

參考資料

  1. # 為什么阿里巴巴禁止使用Apache Beanutils進行屬性的copy弟孟?
  2. 丟棄掉那些BeanUtils工具類吧,MapStruct真香?咚!震糖!
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末论咏,一起剝皮案震驚了整個濱河市厅贪,隨后出現(xiàn)的幾起案子雅宾,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜀变,死亡現(xiàn)場離奇詭異,居然都是意外死亡尊沸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屁商,“玉大人,你說我怎么就攤上這事雾袱」倩梗” “怎么了望伦?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵腿箩,是天一觀的道長。 經(jīng)常有香客問我珠移,道長末融,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任垢乙,我火速辦了婚禮追逮,結(jié)果婚禮上粹舵,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好诅需,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布荧库。 她就那樣靜靜地躺著分衫,像睡著了一般蚪战。 火紅的嫁衣襯著肌膚如雪铐懊。 梳的紋絲不亂的頭發(fā)上科乎,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天喜喂,我揣著相機與錄音,去河邊找鬼玉吁。 笑死进副,一個胖子當著我的面吹牛影斑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼残邀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起空免,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扼菠,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體循榆,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡氮昧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年袖肥,在試婚紗的時候發(fā)現(xiàn)自己被綠了振劳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片历恐。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡弱贼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吮旅,到底是詐尸還是另有隱情庇勃,我是刑警寧澤责嚷,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布罕拂,位于F島的核電站聂受,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蛋济。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一渡处、第九天 我趴在偏房一處隱蔽的房頂上張望医瘫。 院中可真熱鬧,春花似錦稼锅、人聲如沸僚纷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痊臭。三九已至,卻和暖如春广匙,著一層夾襖步出監(jiān)牢的瞬間鸦致,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工总棵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人情龄。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓骤视,卻偏偏與公主長得像专酗,于是被迫代替她去往敵國和親睹逃。 傳聞我的和親對象是個殘疾皇子沉填,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345