使用MapStruct进行类对象拷贝

基本用法

假设我们有两个类需要进行互相转换,分别是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,首先需要依赖他的相关的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需要在编译器生成转换代码,所以需要在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方法,方法的入参类型是PersonDO,出参类型是PersonDTO,这个方法就用于将PersonDO转成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);

    }

输出结果:

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

假如存在名称字段不一致的情况需要映射应该怎么处理呢?下面通过一个案例加以说明,例如:

@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);

    }

运行结果:

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

如果待转换类中存在子类的属性需要赋值给其他类的属性应该怎么做呢?

@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;
}

编写类型转换类:

@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);

    }

运行结果:

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

加入希望在转换的同时对日期格式进行格式化,PersonDTO中新增了一个formatDate字段用以表示格式化后的日期:

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

改写转换类,

@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);
}

运行单元测试类,结果如下:

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,且被被复制类中没有该属性,例如:

@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;
}

撰写转换类:

@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);
}

运行结果:

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)

如果我们希望在类属性进行转换的过程中进行一些更加自定义的操作,应该如何基于mapconstruct的转换类进行扩展呢?切看下面的一个简单的示例:

新增类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);
    }
}

运行结果:

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"})

实现原理

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在运行期的性能会很好,并且还有一个好处,那就是可以把问题的暴露提前到编译期。

使得如果代码中字段映射有问题,那么应用就会无法编译,强制开发者要解决这个问题才行。

学会查看编译后的源代码还是可以帮助我们解决不少问题的,下面以一个笔者在实际使用过程中遇到和解决问题的经历解释一下如何去查看编译后的源码并且借此解决问题的。

首先,像上面提到的进的经典用法一下,笔者写了一个Converter的转换类。

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

但是在编译的时候却产生了如下的报错:


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

发现正常可以编译成功的是直接new一个对象然后往里面挨个set值,但是下面出现问题类,因为子类使用了lombok的@Builder注解,但是@Builder注解的副作用在于无法将父类的属性加入到builder模式中,导致在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真香!!!
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259