关于java:丢弃掉那些BeanUtils工具类吧MapStruct真香

在前几天的文章《为什么阿里巴巴禁止应用Apache Beanutils进行属性的copy?》中,我已经对几款属性拷贝的工具类进行了比照。

而后在评论区有些读者反馈说MapStruct才是真的香,于是我就抽时间理解了一下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 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的性能

后面说了这么多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,他的用法比较简单,并且性能十分欠缺,能够应酬各种状况的字段映射。

并且因为他是编译期就会生成真正的映射代码,使得运行期的性能失去了大大的晋升。

强烈推荐,真的很香!!!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理