MapStruct 实践

不做无用功

一、背景

  • 开发过程中遇到的痛点问题:
    1. 来源类与目标类字段名不一致,举例具体表现为:将电商报名客户的信息,更新到商机客人时,会出现来源的客户号码属性名为: customerPhone,而目标的客户号码属性为:phone等等。
    2. 在某些跨库查询的需求中,需要聚合大家居库以及服务平台库中的两个业务实体的数据,查询并返回某个客户号码的沟通记录,并且要求带上此客户对应的意向潜客信息,即:查询接口返回的 VO 对象,需要返回服务平台的沟通记录,以及大家居系统的意向潜客信息,并且组合起来返回。
    3. 在生产编码过程中,基于安全原则,我们不能将我们数据库字段毫无保留的所有都返回给接口调用方,会约定相应的返回类以及收参类。
  • 因此,在整个过程中,或多或少的,会有属性复制的操作,而在我们项目中,用的较多的是 Apache 提供的工具类:org.apache.commons.beanutils.BeanUtils.copyProperties(Object dest, Object orig) 方法来复制属性;
  • 网上有许多公开资料显示,Apache 提供的 BeanUtils 工具类,存在严重的性能问题,参考图例:
  • 纵坐标是对应操作次数的耗时,横坐标是不同拷贝类属性方法的工具。
  • 可以看出最省事的做法就是,将原有的org.apache.commons.beanutils.BeanUtils.copyProperties 替换为 org.springframework.beans.BeanUtils.copyProperties,Spring 所提供的 BeanUtils 工具类。
  • 但是执行效率,以及代码可调试性、可读性最高的依然还是原生的getter/setter方法。

二、MapStruct 之前

  • MapStruct 是一款专门用来处理 domain 实体类与 Model 类的属性映射的插件,我们只需要定义 Mapper 接口,MapStruct 就会在编译时自动的实现这个映射接口,避免麻烦复杂的映射实现。

MapStruct is a Java annotation processor for the generation of type-safe and performant mappers for Java bean classes. It saves you from writing mapping code by hand, which is a tedious and error-prone task. The generator comes with sensible defaults and many built-in type conversions, but it steps out of your way when it comes to configuring or implementing special behavior.

  • 在编译时 MapStruct 将生成此接口的实现。 生成的实现使用纯 Java 方法调用来映射源对象和目标对象,即不涉及反射。 默认情况下,如果属性在来源类和目标类中具有相同的名称,则它们会被映射,也可以使用 @Mapping 和一些其他注释来自定义映射关系。

三、功能点介绍

对比其他属性映射框架的优势:

  1. 执行效率高: 使用在编译时生成的原生get/set方法,而不是使用反射的方法来实现;
  2. 编译时类型安全: 只能映射相互映射的对象和属性;
  3. 自我完备: 没有外部依赖
  4. 项目构建时,能够及时报告错误的映射关系。
  5. 简单且可调试

开发环境要求:

  • Java 1.8+

四、项目实践

1. 引入依赖

  • 如果是基于 Maven 的项目,在 pom.xml 文件中添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
...

<properties>
<org.mapstruct.version>1.4.2.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>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

...
  • 如果是基于 Gradle 的项目,则需要在项目的 build.gradle 文件中添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
plugins {
...
id "com.diffplug.eclipse.apt" version "3.26.0" // Only for Eclipse
}

dependencies {
...
compile 'org.mapstruct:mapstruct:1.4.2.Final'

annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
testAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final' // if you are using mapstruct in test code
}
...

2. 示例

(1) 简单的映射关系

  1. 假设我们有一个来源类:新电商客户——NewCustomer,继承自基类——BaseBO
1
2
3
4
5
6
7
8
9
10
11
12
13
// BaseBO.java
@Data // Lombok注解
public class BaseBO implements Serializable {

private static final long serialVersionUID = -1;

private String id;// Id
private String createdBy; // 创建人Id
private Date createdTime; //创建日期
private Date lastUpdateTime;//最后更新时间
private String lastUpdatedBy;//最后更新人Id
private Integer rowVersion;//更新次数
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// NewCustomer.java
@Data // Lombok注解
public class NewCustomer extends BaseBO {
private String customerName;// 客户姓名
private String customerPhone;// 客户电话
private String followCustomerServiceName;//跟进客服姓名
private String dealerStatus; // 经销商状态
private String dealerName; // 经销商名称
private String cusJobStatus;//客服是否在职
private String createdUserName; // 创建人名称
private String groupName;// 技能组名称
private String lastupdaterName;// 最后更新人名称
private String shopName; // 门店名称
private String guideName; // 导购姓名
}
  1. 假设我们有一个目标类:虚拟客户VO——VirtualCustomerVO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// VirtualCustomerVO.java
@Data
public class VirtualCustomerVO {
// 虚拟客户ID
private String id;
// 客户姓名
private String customerName;
// 客户电话
private String customerPhone;
// 跟进客服姓名
private String followCustomerServiceName;
// 创建人ID
private String createdBy;
// 创建人姓名
private String createdUserName;
// 创建时间
private Date createdTime;
// 最后更新时间
private Date lastUpdateTime;
}
  1. 定义映射接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// IMapping.java 公共对象映射接口
@MapperConfig
public interface IMapping<SOURCE, TARGET>{
/**
* 映射同名属性
*/
TARGET sourceToTarget(SOURCE var1);
/**
* 反向,映射同名属性
*/
@InheritInverseConfiguration(name = "sourceToTarget")
SOURCE targetToSource(TARGET var1);
/**
* 映射同名属性,集合形式
*/
@InheritConfiguration(name = "sourceToTarget")
List<TARGET> sourceToTarget(List<SOURCE> var1);
/**
* 反向,映射同名属性,集合形式
*/
@InheritConfiguration(name = "targetToSource")
List<SOURCE> targetToSource(List<TARGET> var1);
/**
* 映射同名属性,集合流形式
*/
List<TARGET> sourceToTarget(Stream<SOURCE> stream);
/**
* 反向,映射同名属性,集合流形式
*/
List<SOURCE> targetToSource(Stream<TARGET> stream);
}
1
2
3
4
5
6
// NewCustomer 与 VirtualCustomerVO 映射关系接口
@Mapper(componentModel = "default", unmappedSourcePolicy = ReportingPolicy.IGNORE, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface VirtualCustomerMapping extends IMapping<NewCustomer, VirtualCustomerVO> {

IMapping<NewCustomer, VirtualCustomerVO> INSTANCE = Mappers.getMapper(VirtualCustomerMapping.class);
}

Marks an interface or abstract class as a mapper and activates the generation of a implementation of that type via MapStruct.

  • @Mapper : 将接口类或抽象类标记为由 MapStruct 来实现的映射类。
  1. 测试验证:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// NewCustomerServiceTest.java

public class NewCustomerServiceTest{

private NewCustomer mockSourceData(){
NewCustomer sourceEntity = new NewCustomer();
sourceEntity.setId(UUID.fastUUID().toString(true));
sourceEntity.setCreatedBy("lvtao");
sourceEntity.setCreatedTime(new Date());
sourceEntity.setLastUpdateTime(new Date());
sourceEntity.setLastUpdatedBy("lvtao1");
sourceEntity.setRowVersion(1);
sourceEntity.setCustomerName("感冒灵");
sourceEntity.setCustomerPhone("13888888888");
sourceEntity.setFollowCustomerServiceName("张三");
sourceEntity.setDealerStatus("有效");
sourceEntity.setDealerName("广州索菲亚");
sourceEntity.setCusJobStatus("在职");
sourceEntity.setCreatedUserName("李四");
sourceEntity.setGroupName("广州售后组");
sourceEntity.setLastupdaterName("王五");
sourceEntity.setShopName("索菲亚正佳店");
sourceEntity.setGuideName("导购老王");
return sourceEntity;
}

@Test
public void testMapping() {
NewCustomer source = mockSourceData();
System.out.println("source: ");
System.out.println(JsonUtils.toJson(source));
VirtualCustomerVO target = VirtualCustomerMapping.INSTANCE.sourceToTarget(source);
System.out.println("target: ");
System.out.println(JsonUtils.toJson(target));
}
}
  1. 测试结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
source: 
{
"id": "75c159f307624d5293c90ba3551aacb1",
"createdBy": "lvtao",
"createdTime": "2022-06-01 16:05:34",
"lastUpdateTime": "2022-06-01 16:05:34",
"lastUpdatedBy": "lvtao1",
"rowVersion": 1,
"customerName": "感冒灵",
"customerPhone": "13888888888",
"followCustomerServiceName": "张三",
"dealerStatus": "有效",
"dealerName": "广州索菲亚",
"cusJobStatus": "在职",
"createdUserName": "李四",
"groupName": "广州售后组",
"lastupdaterName": "王五",
"shopName": "索菲亚正佳店",
"guideName": "导购老王"
}
target:
{
"id": "75c159f307624d5293c90ba3551aacb1",
"customerName": "感冒灵",
"customerPhone": "13888888888",
"followCustomerServiceName": "张三",
"createdBy": "lvtao",
"createdUserName": "李四",
"createdTime": "2022-06-01 16:05:34",
"lastUpdateTime": "2022-06-01 16:05:34"
}

(2) 不同成员属性名称的映射关系

  • 有些时候,有些需求,需要查询几个不同业务库的数据,返回接口调用方,比如说,“客人姓名”字段,在来源类中定义的可能是 customerName,而在目标类中定义的可能是 cusName,这种情况在业务需求开发过程中是非常常见的。
  • 实现方式:
1
2
3
4
5
6
7
8
9
10
// NewCustomer 与 VirtualCustomerVO 映射关系接口
@Mapper(componentModel = "default", unmappedSourcePolicy = ReportingPolicy.IGNORE, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface VirtualCustomerMapping extends IMapping<NewCustomer, VirtualCustomerVO> {

IMapping<NewCustomer, VirtualCustomerVO> INSTANCE = Mappers.getMapper(VirtualCustomerMapping.class);

@Override
@Mapping(source = "customerName", target = "cusName")
VirtualCustomerVO sourceToTarget(NewCustomer source);
}
  • 测试验证:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// NewCustomerServiceTest.java

public class NewCustomerServiceTest{

private NewCustomer mockSourceData(){
...
sourceEntity.setCustomerName("感冒灵");
...
return sourceEntity;
}

@Test
public void testMapping() {
NewCustomer source = mockSourceData();
System.out.println("source: ");
System.out.println(JsonUtils.toJson(source));
VirtualCustomerVO target = VirtualCustomerMapping.INSTANCE.sourceToTarget(source);
System.out.println("target: ");
System.out.println(JsonUtils.toJson(target));
}
}
  • 测试结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
source: {
"id": "eed438f1370b4a43bef4642b8772de85",
"createdBy": "lvtao",
"createdTime": "2022-06-01 17:50:52",
"lastUpdateTime": "2022-06-01 17:50:52",
"lastUpdatedBy": "lvtao1",
"rowVersion": 1,
"customerName": "感冒灵",
"customerPhone": "13888888888",
"followCustomerServiceName": "张三",
"dealerStatus": "有效",
"dealerName": "广州索菲亚",
"cusJobStatus": "在职",
"createdUserName": "李四",
"groupName": "广州售后组",
"lastupdaterName": "王五",
"shopName": "索菲亚正佳店",
"guideName": "导购老王"
}
target:
{
"id": "eed438f1370b4a43bef4642b8772de85",
"cusName": "感冒灵",
"customerPhone": "13888888888",
"followCustomerServiceName": "张三",
"createdBy": "lvtao",
"createdUserName": "李四",
"createdTime": "2022-06-01 17:50:52",
"lastUpdateTime": "2022-06-01 17:50:52"
}

(3) 聚合多个来源类到一个目标类中

  • 有种情况,目标类的属性由两个不同的来源类组成,这种情况 MapStruct 也能轻松完成;
  • 例如我们有一个客户扩展类——NewCustomerExt,里面的属性有 age、sex、address:
1
2
3
4
5
6
7
8
9
10
// NewCustomerExt.java 客户扩展类
@Data
public class NewCustomerExt {
// 客户年龄
private Integer age;
// 客户性别
private String sex;
// 客户住址
private String address;
}
  • 修改目标类:
1
2
3
4
5
6
7
8
9
10
11
// VirtualCustomerVO.java
@Data
public class VirtualCustomerVO {
...
// 客户年龄
private Integer age;
// 客户性别
private String sex;
// 客户住址
private String address;
}
  • 修改映射接口
1
2
3
4
5
6
7
8
9
10
11
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE, unmappedSourcePolicy = ReportingPolicy.IGNORE)
public interface VirtualCustomerMapping extends IMapping<NewCustomer, VirtualCustomerVO> {
VirtualCustomerMapping INSTANCE = Mappers.getMapper(VirtualCustomerMapping.class);

@Override
@Mapping(source = "customerName", target = "cusName")
VirtualCustomerVO sourceToTarget(NewCustomer var1);

@Mapping(source = "var1.customerName", target = "cusName")
VirtualCustomerVO multiSourceToTarget(NewCustomer var1, NewCustomerExt var2);
}
  • 测试验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class NewCustomerServiceTest {
...
@Test
public void testMultiMapping(){
NewCustomer source = mockSourceData();
NewCustomerExt sourceExt = new NewCustomerExt();
sourceExt.setAddress("广州市天河区体育东路108号");
sourceExt.setAge(18);
sourceExt.setSex("男");
VirtualCustomerVO target = VirtualCustomerMapping.instance.multiSourceToTarget(source, sourceExt);
System.out.println("source: ");
System.out.println(JsonUtils.toJson(source));
System.out.println("sourceExt: ");
System.out.println(JsonUtils.toJson(sourceExt));
System.out.println("target: ");
System.out.println(JsonUtils.toJson(target));
}
}
  • 测试结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
source: 
{
"id": "6509ce8263a044b49d9d0804a249a041",
"createdBy": "lvtao",
"createdTime": "2022-06-01 18:19:41",
"lastUpdateTime": "2022-06-01 18:19:41",
"lastUpdatedBy": "lvtao1",
"rowVersion": 1,
"customerName": "感冒灵",
"customerPhone": "13888888888",
"followCustomerServiceName": "张三",
"dealerStatus": "有效",
"dealerName": "广州索菲亚",
"cusJobStatus": "在职",
"createdUserName": "李四",
"groupName": "广州售后组",
"lastupdaterName": "王五",
"shopName": "索菲亚正佳店",
"guideName": "导购老王"
}
sourceExt:
{
"age": 18,
"sex": "男",
"address": "广州市天河区体育东路108号"
}
target:
{
"id": "6509ce8263a044b49d9d0804a249a041",
"cusName": "感冒灵",
"customerPhone": "13888888888",
"followCustomerServiceName": "张三",
"createdBy": "lvtao",
"createdUserName": "李四",
"createdTime": "2022-06-01 18:19:41",
"lastUpdateTime": "2022-06-01 18:19:41",
"age": 18,
"sex": "男",
"address": "广州市天河区体育东路108号"
}

(4) 映射类型格式化

  • MapStruct 支持来源类和目标类之间的数据类型转换。它还提供了基本类型及响应的包装类型之间的自动转换。
  • intString 的转换:
1
2
3
4
5
6
7
8
9
// 来自官方示例:
@Mapper
public interface CarMapper{
@Mapping(source = "price", numberFormat = "$#.00")
CarDto carToCarDto(Car car);

@IterableMapping(numberFormat = "$#.00")
List<String> prices(List<Integer> prices);
}
  • BigDecimalString 的转换:
1
2
3
4
5
6
7
// 来自官方示例
@Mapper
public interface CarMapper{
// 科学计数法,例如: 有数字: 10086, 格式化以后就是: 10.09E3 (=> 10.09 * 10^3)
@Mapping(source = "power", numberFormat = "#.##E0")
CarDto carToCarDto(Car car);
}
  • 从日期类型到字符串的转换:
1
2
3
4
5
6
7
8
9
// 来自官方示例
@Mapper
public interface CarMapper {
@Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")
CarDto carToCarDto(Car car);

@IterableMapping(dateFormat = "dd.MM.yyyy")
List<String> stringListToDateList(List<Date> dates);
}

(5) 集合、Stream映射

  • 集合类型的映射与映射 Bean 类型的方式相同,即通过在映射器接口中定义来源类和目标类的映射方法。MapStruct 支持 Java 集合框架中的各种可迭代类型。
  • 生成的代码将包含一个循环,该循环遍历来源集合,把每一个元素转换后放入目标集合中。如果在给定的映射器或其使用的映射器中找到集合元素类型的映射方法,则调用此方法来执行元素转换。
  • 示例:
1
2
3
4
5
6
7
8
// 如果我们的映射器接口是继承自 IMapping.java 接口类的话,可以直接调用方法sourceToTarget()   例如:
@Test
public void testCollectionMapping(){
List<NewCustomer> sourceList = Lists.newArrayList();
List<VirtualCustomerVO> targetList = Lists.newArrayList();
targetList = VirtualCustomerMapping.INSTANCE.sourceToTarget(sourceList);
List<VirtualCustomerVO> virtualCustomerVOS = VirtualCustomerMapping.INSTANCE.sourceToTarget(sourceList.stream());
}

五、总结

  • MapStruct 是一个用于创建映射器的库,从基本映射到自定义方法和自定义映射器,此外, 我们还介绍了MapStruct提供的一些高级操作选项,包括依赖注入,数据类型映射和表达式使用。
  • 执行效率高,接近原生的 get/set 方法的执行效率;

参考资料