0%

MapStruct的简单使用

前言

因项目的DTO越来越多,且不同层级的DTO和javabean需要转换,大量使用bean.copy方法,造成性能下降,阅读性差的问题,准备引入MapStruct进行javabean数据的映射转换。

MapStruct是什么

MapStruct是一个Java注释处理器,用于为Java bean类生成类型安全和高性能的映射器。它使您不必手工编写映射代码,这是一个繁琐且容易出错的任务。该生成器具有合理的默认值和许多内置的类型转换,但是在配置或实现特殊行为时,它会自动退出。

优点

  1. 通过使用普通方法调用而不是反射来快速执行
  2. 编译时类型安全。只能映射彼此映射的对象和属性,因此不会将实体意外映射到客户DTO等等。
  3. 独立代码-没有运行时依赖
  4. 在构建时清除错误报告,如:
    a. 映射不完整(并非所有目标属性都被映射)
    b. 映射不正确(找不到正确的映射方法或类型转换)
  5. 易于调试的映射代码(或可以手动编辑,例如,如果生成器中有错误)

运行环境

这里使用的是MapStruct 1.3.1版本,需要Java 1.8或以上(1.2.x版本好像不支持Java 1.8,需要其他包代替)

简单说明

需要在两种类型之间创建映射,请声明一个映射器类,如下所示:

1
2
3
4
5
6
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
}

在编译时,MapStruct将生成此接口的实现。生成的实现使用简单的Java方法调用在源对象和目标对象之间进行映射,即不涉及反射。默认情况下,如果属性在源和目标中具有相同的名称,则将映射它们,但是您可以使用@Mapping和少量其他注释来控制此属性和许多其他方面。

开发工具插件

IntelliJ idea 中可在插件市场中下载使用插件(https://plugins.jetbrains.com/plugin/10036-mapstruct-support
PS:这里用到Lombok,因此也需要下载idea插件市场的一个Lombok插件

确保您idea版本至少为 2018.2.x(由于对maven-compiler-plugin的注解处理器的支持来自该版本,因此是必需的)。
在IntelliJ中启用注释处理(Build, Execution, Deployment -> Compiler -> Annotation Processors)

Maven环境配置

对于基于Maven的项目,将以下内容添加到您的POM文件中以使用MapStruct(依赖关系在Maven Central中可用):

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
41
...
<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.0</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>

<!--添加Lombok的注册处理器-->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
...

碰到的问题

当MapStruct和Lombok混合使用时,会出现异常,找不到字段:
Warning:java: 来自注释处理程序 'net.java.dev.hickory.prism.internal.PrismGenerator' 的受支持 source 版本 'RELEASE_6' 低于 -source '1.8'
Error:(101, 16) java: No property named "numberOfSeats" exists in source parameter(s). Did you mean "null"?

解决方法只需要在<maven-compiler-plugin>节点的<annotationProcessorPaths>中添加:

1
2
3
4
5
6
<!--添加Lombok的注册处理器-->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>

下面是代码

需要 说明
源实体 Car 数据来源实体,数据库实体(this) 转换到 DTO
目标实体 CarDto 转换目标实体,DTO 转换到 响应实体(this)
映射interface CarMapper 定义用于转换映射的interface,MapStruct会实现这个接口
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package raven.spring.study;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.ToString;
import org.junit.jupiter.api.Test;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

class MapStructTests {

@Test
public void shouldMapCarToDtoTest() {

// 实例化数据对象
Car car = new Car( "Morris", 5, CarType.SEDAN ,200.19);

// 获得映射实例, 并调用转换方法
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

System.out.println(car);
System.out.println(carDto);

System.out.println("同类型同名:" + carDto.getMake().equals("Morris"));
System.out.println("同类型非同名" + (carDto.getSeatCount() == (5)));
System.out.println("字符串-枚举:" + carDto.getType().equals("SEDAN"));
System.out.println("非同类型非同名:" + carDto.getMaxCarSpeed().equals(String.valueOf(car.getMaxSpeed())));

/*
打印结果:
Car{make='Morris', numberOfSeats=5, type=CarType{key='SEDAN'}, maxSpeed=200.19}
CarDto{make='Morris', seatCount=5, type='SEDAN', maxCarSpeed='200.19'}
同类型同名:true
同类型非同名true
字符串-枚举:true
非同类型非同名:true
*/

}

}

// 数据实体->DTO 两种类型非常相似,只有座位数属性具有不同的名称,并且类型属性在Car类中具有特殊的枚举类型,但在DTO中是纯字符串。

/**
* 数据实体
*/

@Data
@AllArgsConstructor
@ToString
class Car {

private String make;
private int numberOfSeats;
private CarType type;
private double maxSpeed;
}

/**
* 传输DTO
*/

@Data
@ToString
class CarDto {

private String make;
private int seatCount;
private String type;
private String maxCarSpeed;
}


/**
* 枚举
*/

@AllArgsConstructor
@Getter
enum CarType {
SEDAN("SEDAN");
private String key;
}

/**
* 映射器
* 从Car对象中创建CarDto对象,需要定义一个映射器接口
*/

// @Mapper将接口标记为映射接口,并让MapStruct处理器在编译期间启动。
// 对于源对象和目标对象中名称不同的属性,@Mapping可用于配置名称。
// 当然,一个接口中可以有多种映射方法,MapStruct将为所有接口生成一个实现。
@Mapper
interface CarMapper {

// 可以从Mappers类检索接口实现的实例。按照约定,该接口声明一个成员INSTANCE,为客户端提供对映射器实现的访问。
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

// 实际的映射方法将源对象作为参数并返回目标对象。其名称可以自由选择。
// 在必要和可能的情况下,将对源和目标中具有不同类型的属性执行类型转换,例如type属性将从枚举类型转换为字符串。
@Mapping(source = "numberOfSeats", target = "seatCount")
@Mapping(source = "maxSpeed", target = "maxCarSpeed")
CarDto carToCarDto(Car car);
}

总结

使用起来还是挺方便的,只需要新建一个映射器即可使用,不同的类型或者字段也能标记映射,阅读性更高,维护性也高。