前言
今天我们来介绍一款非常优秀的实体对象映射工具,MapStruct
。
在开发的时候我们会看到业务代码之间有很多的JavaBean
之间的相互转化,非常影响美观,但是又不得不存在。
通常的JavaBean
处理方法是通过Spring或者Apache提供的BeanUtils
工具,而对于一些不匹配的属性,通过get/set
方法解决。
由于BeanUtils
使用反射机制,故其在大量使用时可能会影响到性能,同时对于不匹配的属性,如果较多,get/set
起来也非常麻烦和繁琐。
对于一些特殊类型不一致的字段,如DO
里为Date
类型,可能DTO
里需要变为时间的String
类型,我们需要调用指定方法进行处理,再进行get/set
。
正文
而MapStruct
可以让我们从这种复杂而繁琐的工作中解放出来。
MapSturct
是一个生成类型安全,高性能且无依赖的JavaBean
映射代码的注解处理器(annotation processor)。
它可以让我们通过注解的方式,生成JavaBean
之间的映射代码,相较于反射,更安全更高效。
让我们来看一下。
MapStruct引入
首先我们需要在Maven
项目中引入MapSturct
的相关Jar包。
1 | <properties> |
有两个,mapstruct
是主要包,mapstruct-processor
用来辅助生成JavaBean
之间的映射代码。
MapStruct使用
我们根据例子来看如何使用MapSturct
。
我们这儿有两个JavaBean
,PersonDO
和PersonDTO
,现在我们将PersonDO
转为PersonDTO
。
1 |
|
1 |
|
1 | public enum Gender { |
转化时需要编写Mapper
,即对象映射器,是一个接口,如下。
1 |
|
使用注解@Mapper
定义一个Converter
接口,在其中定义一个do2dto
方法,方法的入参类型是PersonDO
,出参类型是PersonDTO
,这个方法就用于将PersonDO
转成PersonDTO
。
测试代码如下:
1 | public class Test { |
输出结果
1 | PersonDTO(userName=Sakura, age=27, gender=MAN) |
由上面的例子可以看到MapSturct
成功将PersonDO
转成PersonDTO
。
MapStruct字段映射处理
两个JavaBean
对象进行映射,属性名称一致的字段可以相互映射,不一致的如何处理呢?
两个字段名字不一致
从上面的例子可以看出,我们只需要使用@Mapping
注解。在转换方法上设置
1 | "name", target = "userName") (source = |
即可。其含义是将name
字段的值映射给userName
。
MapStruct可以映射的类型
在上面例子,我们发现age
字段完成映射,其类型一个为int
,另一个为包装类。
gender
字段完成映射,其类型一个为枚举,一个为String
。
一般情况下,MapSturct
会对部分类型做自动映射,而不需要我们额外配置。
- 基本类型及其他们对应的包装类型。
- 基本类型的包装类型和String类型之间
- String类型和枚举类型之间
自定义常量和默认值
如果我们想在转换中,给某个属性一个固定值,可以使用constant
。
比如PersonDTO
里新增身高字段。而PersonDO
里没有,那么我们可以在转换时赋予默认值。
1 |
|
1 | "height",constant = "175") (target = |
而对于某一个属性,如果映射后为空,我们可以给予其默认值,使用defaultValue
,如下:
1 | "gender",defaultValue = "MAN") (target = |
类型不一致的映射
在实际转化中,有可能字段类型是不一致的,如何进行映射呢?
我们使用上面的例子,在PersonDO
里增加生日属性,为Date
类型,PersonDTO
里为String
类型。
1 |
|
1 |
|
这时候我们可以使用如下方法转化。
1 | "name", target = "userName") (source = |
运行后可以看到正确输出结果:
1 | PersonDTO(userName=Sakura, age=27, gender=MAN, height=175, birthday=2020-08-17 15:20:16) |
dateFormat
是MapStruct
自带的属性,如果我们的属性映射方法比较复杂呢?
当然,MapStruct
支持我们自定义属性转换的方法,我们来看一下。
我们新增一个兴趣爱好字段,如下:
1 |
|
需要完成List
对List
的转化,我们可以使用如下代码:
1 |
|
PS:java1.8接口支持自定义default方法,expression = “java(getHobbiesStr(person.getHobbies()))”
这段代码也可以指向自己写的某个方法,需要具体路径包名。
上面代码还是非常好理解的,这儿就不过多叙述。
多个属性映射
MapStruct
还支持将多个实体数据组装到一个实体中,如下:
比如上面的例子,我们的身高属性来自另一个Bean
,则代码如下:
1 |
|
则组装数据的方法如下:
1 | "person.name", target = "userName") (source = |
对于source
属性,需要指定来自哪个DO
。
1 | public class Test { |
输出结果:
1 | PersonDTO(userName=Sakura, age=27, gender=MAN, height=178, birthday=2020-08-17 15:52:14, strHobbies=[足球, 玩游戏]) |
更新现有Bean实例
MapStruct
还支持将一个Bean
的属性更新到另一个Bean
的同名属性里。
1 | void updateDTOfromDO(PersonDO person, @MappingTarget PersonDTO personDTO); |
需要注意这个对于同名属性,如果DO
上有值,那么DTO
上的值将被覆盖,如下:
1 | PersonDTO dto = new PersonDTO(); |
输出结果:
1 | PersonDTO(userName=null, age=27, gender=MAN, height=null, birthday=2020/8/17 下午4:29, strHobbies=null) |
继承反转配置
MapStruct
在存在DO
转换为DTO
的前提下,还支持将DTO
转换为DO
,而且不用我们复杂的配置。
方法如下:
1 |
|
我们只需要@InheritInverseConfiguration
注解即可解决,name
属性指向DO
转换为DTO
的方法。
测试:
1 | PersonDTO dto1 = new PersonDTO(); |
输出:
1 | PersonDO(id=null, name=Sakura, age=27, gender=MAN, birthday=Sun Nov 11 12:12:12 CST 2018, hobbies=null) |
这儿需要注意的是我们看到Hobbies
属性并没有转化,这是正常的,这个需要我们配置@Mapping
,写出并指定兴趣爱好转化的逆向方法。
MapStruct的性能
MapStruct
的性能是非常优秀的,我们来测试一下:
为简化代码,我们是DO
和DTO
尽可能的简单。
1 |
|
1 |
|
MapStruct
相关方法:
1 |
|
我们使用Spring
自带的BeanUtils
工具类来与其进行比较:
1 | public class Test { |
我们以100000个对象为样本,得到如下输出结果:
1 | BeanUtils属性拷贝耗时:1903ms |
可以看到差距是非常大的。
MapStruct
为什么有如此优秀的性能呢?
其实核心点在于:MapStruct
在编译期间,就生成了对象映射代码,确保了高性能,同时编译期间也可以发现可能存在的映射问题。
在上面的TestConverter
中,经过代码编译后,MapStruct
会生成一个TestConverterImpl
的实现类,帮我们进行对象属性转换。
如下:
1 | ( |
这样在运行之前就相当于我们手写get/set
方法,相比反射,速度更快。
其编译工作主要由mapstruct-processor
包完成。
PS:PersonConverterImpl
的相关实现这儿就不展示了,大家可以看下它是如何处理的,比如对于一些自定义方法等。
总结
本文介绍了一款Java对象映射工具,MapStruct
。相比传统BeanUtils
有着更高的性能。
映射代码在编译期间生成,相当于替我们手写了get/set
,不仅相比反射具有很好的性能,同时还可以在编译期间发现可能存在的映射问题。