丢弃掉那些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源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索