MapStruct 官网:https://mapstruct.org/
1. 痛点
一种框架的出现都要解决个痛点,我想下面这这种不方便的操作经常有人写吧。
假如Car
类是数据库映射类:
1 | package cn.felord.mapstruct.entity; |
CarType
类:
1 | package cn.felord.mapstruct.entity; |
CarDTO
是DTO类:
1 | package cn.felord.mapstruct.entity; |
我们从数据库查询Car
然后需要转换为CarDTO
,通常我们会这么写一个方法进行转换:
1 | public CarDTO carToCarDTO(Car car) { |
这种写法非常繁琐无味,而且没有技术含量。甚至中间还牵涉了很多类型转换,嵌套之类的繁琐操作,而我们想要的只是建立它们之间的映射关系而已。有没有一种通用的映射工具来帮我们搞定这一切。当然有而且还不少。有人说apache的BeanUtil.copyProperties
可以实现,但是性能差而且容易出异常,很多规范严禁使用这种途径。以下是对几种对象映射框架的对比,大多数情况下 MapStruct
性能最高。原理类似于lombok
,MapStruct
都是在编译期进行实现,而且基于Getter
、Setter
,没有使用反射所以一般不存在运行时性能问题。
今天就搞一搞MapStruct, 并跟Spring Boot 2.x 集成以下。 无论是idea 还是eclipse 都建议安装 MapStruct Plugin 插件,当然不安装也是可以的。
2. Spring Boot 2.1.9 集成 MapStruct
在 Spring Boot 的 pom.xml
下引入 MapStruct
的 maven 依赖坐标:
1 | <dependencies> |
3. 使用MapStruct
我们把开始的痛点解决一下。看看 MapStruct 如何降低你的编程成本。
3.1 编写转换源到目标的映射
编写Car
到CarDTO
的映射:
1 | package cn.felord.mapstruct.mapping; |
3.2 MapStruct映射方法讲解
上面短短几行代码就可以了十分简单!解释一下操作步骤:
首先声明一个映射接口用@org.mapstruct.Mapper
(不要跟mybatis注解混淆)标记,说明这是一个实体类型转换接口。这里我们声明了一个 CAR_MAPPING
来方便我们调用,CarDTO toCarDTO(Car car)
是不是很熟悉, 像mybatis
一样抽象出我们的转换方法。@org.mapstruct.Mapping
注解用来声明成员属性的映射。该注解有两个重要的属性:
source
代表转换的源。这里就是Car
。target
代表转换的目标。这里是CarDTO
。
这里以成员变量的参数名为依据,如果有嵌套比如 Car
里面有个 CarType
类型的成员变量 carType
,其 type
属性 来映射 CarDTO
中的 type
字符串,我们使用 type.type
来获取属性值。如果有多层以此类推。MapStruct
最终调用的是 setter
和 getter
方法,而非反射。这也是其性能比较好的原因之一。numberOfSeats
映射到 seatCount
就比较好理解了。我们是不是忘记了一个属性 make
,因为他们的位置且名称完全一致,所以可以省略。而且对于包装类是自动拆箱封箱操作的,并且是线程安全的。MapStruct不单单有这些功能,还有其他一些复杂的功能:
设置转换默认值和常量。当目标值是 null
时我们可以设置其默认值,注意这些都是基本类型以及对应都 boxing
类型,如下
1 | @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined") |
需要注意的是常量不能对源进行引用(不能指定 source
属性),下面是正确的操作:
1 | @Mapping(target = "stringConstant", constant = "Constant Value") |
3.2 Mapper 编译
当你的应用编译后。你会在编译后的目录比如 maven是 target\generated-sources\annotations
下的子目录发现生成了一个实现类 比如 我们上面的CarMapping
会生成CarMappingImpl
如下:
1 | package cn.felord.mapstruct.mapping; |
4. MapStruct 进阶操作
下面介绍几种 MapStruct 的进阶操作:
4.1 格式化操作
格式化也是我们经常使用的操作,比如数字格式化,日期格式化。
这是处理数字格式化的操作,遵循java.text.DecimalFormat
的规范:
1 | @Mapping(source = "price", numberFormat = "$#.00") |
下面展示了将一个日期集合映射到日期字符串集合的格式化操作上:
1 |
|
4.2 使用 java 表达式
下面演示如何使用LocalDateTime
作为当前的时间值注入 addTime
属性中。
首先在@org.mapstruct.Mapper
的 imports
属性中导入 LocalDateTime
,该属性是数组意味着你可以根据需要导入更多的处理类:
1 | @Mapper(imports = {LocalDateTime.class}) |
接下来只需要在对应的方法上添加注解@org.mapstruct.Mapping
,其属性expression
接收一个 java()
包括的表达式:
无入参版本:
1
@Mapping(target = "addTime", expression = "java(LocalDateTime.now())")
携带入参的版本, 我们将
Car
的出厂日期字符串manufactureDateStr
注入到CarDTO
的LocalDateTime
类型属性addTime
中去:1
2@Mapping(target = "addTime", expression = "java(LocalDateTime.parse(car.manufactureDateStr))")
CarDTO carToCarDTO(Car car);
4.3 MapStruct 转换 Mapper 注入Spring IoC 容器
如果使用要把Mapper 注入Spring IoC 容器我们只需要这么声明,不用再构建一个单例,就可以像其他 spring bean一样对CarMapping 进行引用了:
1 | package cn.felord.mapstruct.mapping; |
5.总结
其实MapStruct 还有很多的功能。但是从可读性来说,我建议使用以上几种容易理解的功能即可。如果你感兴趣可以去mapstruct.org进一步学习。配合lombok和我介绍的jsr303,让你更加专注于业务,而且代码更加清晰。