Spring Boot 2 实战:集成 MapStruct 类型转换神器

MapStruct 官网:https://mapstruct.org/

img

1. 痛点

一种框架的出现都要解决个痛点,我想下面这这种不方便的操作经常有人写吧。
假如Car类是数据库映射类:
​​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.felord.mapstruct.entity;

import lombok.Data;

/**
* Car
*
* @author Felordcn
* @since 13:35 2019/10/12
**/
@Data
public class Car {
private String make;
private int numberOfSeats;
private CarType type;

}

CarType 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package cn.felord.mapstruct.entity;

import lombok.Data;

/**
* CarType
*
* @author Felordcn
* @since 13:36 2019/10/12
**/
@Data
public class CarType {
private String type;
}

CarDTO是DTO类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.felord.mapstruct.entity;

import lombok.Data;

/**
* CarDTO
*
* @author Felordcn
* @since 13:37 2019/10/12
**/
@Data
public class CarDTO {
private String make;
private int seatCount;
private String type;
}

我们从数据库查询Car 然后需要转换为CarDTO,通常我们会这么写一个方法进行转换:

1
2
3
4
5
6
7
8
9
public CarDTO carToCarDTO(Car car) {
CarDTO carDTO = new CarDTO();

carDTO.setMake(car.getMake());
carDTO.setSeatCount(car.getNumberOfSeats());
carDTO.setType(car.getCarType().getType());
// 有可能更长
return carDTO;
}

这种写法非常繁琐无味,而且没有技术含量。甚至中间还牵涉了很多类型转换,嵌套之类的繁琐操作,而我们想要的只是建立它们之间的映射关系而已。有没有一种通用的映射工具来帮我们搞定这一切。当然有而且还不少。有人说apache的BeanUtil.copyProperties可以实现,但是性能差而且容易出异常,很多规范严禁使用这种途径。以下是对几种对象映射框架的对比,大多数情况下 MapStruct 性能最高。原理类似于lombokMapStruct都是在编译期进行实现,而且基于GetterSetter,没有使用反射所以一般不存在运行时性能问题。

diff.png

今天就搞一搞MapStruct, 并跟Spring Boot 2.x 集成以下。 无论是idea 还是eclipse 都建议安装 MapStruct Plugin 插件,当然不安装也是可以的。

2. Spring Boot 2.1.9 集成 MapStruct

在 Spring Boot 的 pom.xml 下引入 MapStruct 的 maven 依赖坐标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>compile</scope>
</dependency>
<!-- other dependencies -->
</dependencies>

3. 使用MapStruct

我们把开始的痛点解决一下。看看 MapStruct 如何降低你的编程成本。

3.1 编写转换源到目标的映射

编写CarCarDTO的映射:

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
package cn.felord.mapstruct.mapping;

import cn.felord.mapstruct.entity.Car;
import cn.felord.mapstruct.entity.CarDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
* CarMapping
*
* @author Felordcn
* @since 14 :02 2019/10/12
*/
@Mapper
public interface CarMapping {

/**
* 用来调用实例 实际开发中可使用注入Spring 不写
*/
CarMapping CAR_MAPPING = Mappers.getMapper(CarMapping.class);

/**
* 源类型 目标类型 成员变量相同类型 相同变量名 不用写{@link org.mapstruct.Mapping}来映射
*
* @param car the car
* @return the car dto
*/
@Mapping(target = "type", source = "carType.type")
@Mapping(target = "seatCount", source = "numberOfSeats")
CarDTO carToCarDTO(Car car);
}

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 最终调用的是 settergetter 方法,而非反射。这也是其性能比较好的原因之一。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
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
package cn.felord.mapstruct.mapping;

import cn.felord.mapstruct.entity.Car;
import cn.felord.mapstruct.entity.CarDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import javax.annotation.Generated;
import org.springframework.stereotype.Component;

@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2019-10-12T15:05:36+0800",
comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_222 (Amazon.com Inc.)"
)
@Component
public class CarMappingImpl implements CarMapping {

@Override
public CarDTO carToCarDTO(Car car) {
if ( car == null ) {
return null;
}

CarDTO carDTO = new CarDTO();

carDTO.setType( carCarTypeType( car ) );
carDTO.setSeatCount( car.getNumberOfSeats() );
carDTO.setMake( car.getMake() );

return carDTO;
}

private String carCarTypeType(Car car) {
if ( car == null ) {
return null;
}
CarType carType = car.getCarType();
if ( carType == null ) {
return null;
}
String type = carType.getType();
if ( type == null ) {
return null;
}
return type;
}
}

4. MapStruct 进阶操作

下面介绍几种 MapStruct 的进阶操作:

4.1 格式化操作

格式化也是我们经常使用的操作,比如数字格式化,日期格式化。
这是处理数字格式化的操作,遵循java.text.DecimalFormat的规范:

1
@Mapping(source = "price", numberFormat = "$#.00")

下面展示了将一个日期集合映射到日期字符串集合的格式化操作上:

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

4.2 使用 java 表达式

下面演示如何使用LocalDateTime 作为当前的时间值注入 addTime 属性中。

首先在@org.mapstruct.Mapperimports 属性中导入 LocalDateTime,该属性是数组意味着你可以根据需要导入更多的处理类:

1
@Mapper(imports = {LocalDateTime.class})

接下来只需要在对应的方法上添加注解@org.mapstruct.Mapping ,其属性expression 接收一个 java() 包括的表达式:

  • 无入参版本:

    1
    @Mapping(target = "addTime", expression = "java(LocalDateTime.now())")
  • 携带入参的版本, 我们将 Car 的出厂日期字符串manufactureDateStr 注入到 CarDTOLocalDateTime 类型属性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
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
 package cn.felord.mapstruct.mapping;

import cn.felord.mapstruct.entity.Car;
import cn.felord.mapstruct.entity.CarDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
* CarMapping 注入spring 写法
*
* @author Felordcn
* @since 14 :02 2019/10/12
*/
@Mapper(componentModel = "spring")
public interface CarMapping {

/**
* 用来调用实例 实际开发中可使用注入Spring 不写
*/
// CarMapping CAR_MAPPING = Mappers.getMapper(CarMapping.class);

/**
* 源类型 目标类型 成员变量相同类型 相同变量名 不用写{@link Mapping}来映射
*
* @param car the car
* @return the car dto
*/
@Mapping(target = "type", source = "carType.type")
@Mapping(target = "seatCount", source = "numberOfSeats")
CarDTO carToCarDTO(Car car);
}

5.总结

其实MapStruct 还有很多的功能。但是从可读性来说,我建议使用以上几种容易理解的功能即可。如果你感兴趣可以去mapstruct.org进一步学习。配合lombok和我介绍的jsr303,让你更加专注于业务,而且代码更加清晰。

6.资料

个人微信公众号技术交流QQ群
文章目录
  1. 1. 1. 痛点
  2. 2. 2. Spring Boot 2.1.9 集成 MapStruct
  3. 3. 3. 使用MapStruct
    1. 3.1. 3.1 编写转换源到目标的映射
    2. 3.2. 3.2 MapStruct映射方法讲解
    3. 3.3. 3.2 Mapper 编译
  4. 4. 4. MapStruct 进阶操作
    1. 4.1. 4.1 格式化操作
    2. 4.2. 4.2 使用 java 表达式
    3. 4.3. 4.3 MapStruct 转换 Mapper 注入Spring IoC 容器
  5. 5. 5.总结
  6. 6. 6.资料