知识 - MapStruct
# 介绍
对于代码中 JavaBean之间的转换, 一直是困扰我很久的事情。在开发的时候我看到业务代码之间有很多的 JavaBean 之间的相互转化, 非常的影响观感,却又不得不存在。我后来想的一个办法就是通过反射,或者自己写很多的转换器。
第一种通过反射的方法确实比较方便,但是现在无论是 BeanUtils, BeanCopier 等在使用反射的时候都会影响到性能。虽然我们可以进行反射信息的缓存来提高性能。但是像这种的话,需要类型和名称都一样才会进行映射,有很多时候,由于不同的团队之间使用的名词不一样,还是需要很多的手动 set/get 等功能。
第二种的话就是会很浪费时间,而且在添加新的字段的时候也要进行方法的修改。不过,由于不需要进行反射,其性能是很高的。
针对第二种,我们就可以使用 MapStruct。
MapSturct 是一个生成类型安全,高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。
# 依赖
这里使用的是最新版:(2023-07-12)
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
用到了 Lombok 和 MapStruct 进行搭配。
但是目前这两个同时作为最新版,会有冲突问题,即 Maven 默认使用了 MapStruct 的处理器,导致 Lombok 的所有注解都失效,如 @Setter
、@Getter
注解在和 MapStruct 搭配时,不会生产 Setter、Getter 方法,导致报错。
所以需要告诉 Maven,先使用 Lombok,再使用 MapStruct。
<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>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- This is needed when using Lombok 1.18.16 and above -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
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
如果不使用 Lombok,则需要手动对属性生成 Setter、Getter 等方法。
# 映射方式
# 隐式映射
这里演示从 PersonDTO 转换为 PersonVO。
PersonDTO 类:
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class PersonDTO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
PersonVO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
13
可以看到,两个类除了名字不一样,属性的类型和名字都一样,所以我们用 MapStruct 进行转换的时候是最方便的。
定义一个 PersonConvert 类,用于将 DTO 转为 VO。一般将一个 Package 为 convert,专门存放 MapStruct 转换的类。
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
PersonVO personVOToDTO(PersonDTO personDTO);
}
2
3
4
5
6
@Mapper
是 MapStruct 自带的类,类似于 SpringBoot 的 @Component
等注解,标识该类是 MapStruct 的处理类。
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setId(127);
personDTO.setName("可乐");
personDTO.setAge(24);
personDTO.setAddress("深圳");
personDTO.setBirthDate(new Date());
personDTO.setIsMarried(true);
PersonVO personVO = PersonConvert.INSTANCE.personVOToDTO(personDTO);
System.out.println(personVO);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
打印的结果:
PersonVO(id=127, name=可乐, age=24, address=深圳, birthDate=Tue Jul 11 23:58:06 CST 2023, isMarried=true)
如果我们去看编译后的 class 类可以发现,MapStruct 自动生成了一个 PersonConvertImpl.class
,里面就是 set 和 get 方法,如下:
public class PersonConvertImpl implements PersonConvert {
public PersonConvertImpl() {
}
public PersonVO personVOToDTO(PersonDTO personDTO) {
if (personDTO == null) {
return null;
} else {
PersonVO personVO = new PersonVO();
personVO.setId(personDTO.getId());
personVO.setName(personDTO.getName());
personVO.setAge(personDTO.getAge());
personVO.setAddress(personDTO.getAddress());
personVO.setBirthDate(personDTO.getBirthDate());
personVO.setIsMarried(personDTO.getIsMarried());
return personVO;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这个自动生成的类非常熟悉,就是我们常用的 set 和 get 方法。
# 显式映射
上面的自动映射是当两个类的属性名一样的情况下,MapStruct 自动映射和注入,但是当属性名不一样的时候,我们就需要告诉 MapStruct,Source 类的哪个属性名对应 Target 的哪个属性名。
这个前提是类型保持一致,或者是 int 和 Integer 的能自动转化的关系。
PersonDTO 类:
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
13
PersonVO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private Integer id;
private String personName;
private Integer personAge;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
13
可以看到,DTO 的 name 对应 VO 的 personName,DTO 的 age 对应 VO 的 personAge,但是 MapStcut 并不知道,所以我们需要告诉它。
如下:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mappings(
value = {
@Mapping(source = "name", target = "personName"),
@Mapping(source = "age", target = "personAge")
}
)
PersonVO personVOToDTO(PersonDTO personDTO);
}
2
3
4
5
6
7
8
9
10
11
12
13
用一个 @Mappings
包裹多个 @Mapping
,每个 @Mapping
使用了两个属性:source 和 target。
其中 source 是指定 PersonDTO(参数)的属性名,target 是指定 PersonVO(返回值)的属性名,这样就形成了对应关系,MapStruct 将 source 属性的值注入到 target 属性里。
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setId(127);
personDTO.setName("可乐");
personDTO.setAge(24);
personDTO.setAddress("深圳");
personDTO.setBirthDate(new Date());
personDTO.setIsMarried(true);
PersonVO personVO = PersonConvert.INSTANCE.personVOToDTO(personDTO);
System.out.println(personVO);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
打印结果:
PersonVO(id=127, personName=可乐, personAge=24, address=深圳, birthDate=Wed Jul 12 00:03:00 CST 2023, isMarried=true)
当然,上面 PersonConvert 用 @Mappings
包裹多个 @Mapping
比较麻烦,我们可以直接使用精简版:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO personVOToDTO(PersonDTO personDTO);
}
2
3
4
5
6
7
8
9
直接使用多个 @Mapping
即可。
# 集合映射
当属性是一个集合时,无论使用隐式还是显式映射,MapStruct 都会根据是否是集合来进行自动映射,如:
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private List<Integer> id;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private List<Integer> id;
}
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
PersonVO personVOToDTO(PersonDTO personDTO);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MapStruct 会自动处理集合的映射,底层就是 for 循环处理 Integer 映射。
# 忽略映射
# 指定属性忽略映射
MapStruct 默认是把相同属性名的属性进行映射,但是我们可能不需要某些字段进行映射,那么我们可以用 @Mapping
的 ignore 来告诉 MapStruct 进行忽视,如:
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
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
我们希望不将 PersonDTO 的 name 和 address 转换到 PersonVO 里,则
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(target = "name", ignore = true)
@Mapping(target = "address", ignore = true)
PersonVO convert(PersonDTO personDTO);
}
2
3
4
5
6
7
8
9
运行类:
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setId(127);
personDTO.setName("可乐");
personDTO.setAge(24);
personDTO.setAddress("深圳");
personDTO.setBirthDate(new Date());
personDTO.setIsMarried(true);
PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
System.out.println("转换成功的 personVO:" + personVO);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
打印:
转换成功的 personVO:PersonVO(id=127, name=null, age=24, address=null, birthDate=Sat Jul 15 00:37:10 CST 2023, isMarried=true)
可以看到 name 和 address 都被忽视了。
# 全相等属性忽略映射
有时候我们希望只有 Source 类和 Target 类的属性名一样,则进行忽略,但是如:
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
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
如果我们使用上面的 ignore 进行忽略,则需要写 6 个 @Mapping(target = "xxx", ignore = true)
,这明显不优雅,那么我们可以使用 MapStruct 提供的另一个注解 @BeanMapp
的 ignoreByDefault 属性进行全相等属性忽略映射。
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@BeanMapping(ignoreByDefault = true)
PersonVO convert(PersonDTO personDTO);
}
2
3
4
5
6
7
8
运行类:
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setId(127);
personDTO.setName("可乐");
personDTO.setAge(24);
personDTO.setAddress("深圳");
personDTO.setBirthDate(new Date());
personDTO.setIsMarried(true);
PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
System.out.println("转换成功的 personVO:" + personVO);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
打印:
转换成功的 personVO:PersonVO(id=null, name=null, age=null, address=null, birthDate=null, isMarried=null)
可以看到所有相同名字的属性都被忽视了。
# 指定属性映射
如果我们只想一两个属性进行映射,其他都忽略,则在 全相等属性忽略映射 基础上,使用 @Mapping
即可。
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
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
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@BeanMapping(ignoreByDefault = true)
@Mapping(source = "name", target = "name")
@Mapping(source = "address", target = "address")
PersonVO convert(PersonDTO personDTO);
}
2
3
4
5
6
7
8
9
10
运行类:
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setId(127);
personDTO.setName("可乐");
personDTO.setAge(24);
personDTO.setAddress("深圳");
personDTO.setBirthDate(new Date());
personDTO.setIsMarried(true);
PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
System.out.println("转换成功的 personVO:" + personVO);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
打印:
转换成功的 personVO:PersonVO(id=null, name=可乐, age=null, address=深圳, birthDate=null, isMarried=null)
可以看到 name、address 属性进行映射,其他属性被忽略映射。
# 获取 Mapper 实例
我们如何获取经过 @Mapper
修饰的类呢?上面的例子其实以及说明了,通过 Mappers.getMapper(xxx.class)
的方式来进行对应 Mapper 的获取。此种方法为通过 Mapper 工厂获取。
如果是此种方法,约定俗成的是在接口内定义一个接口本身的实例 INSTANCE,以方便获取对应的实例。
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
PersonVO personVOToDTO(PersonDTO personDTO);
}
2
3
4
5
6
当然我们也可以在外面的类通过 Mappers.getMapper(PersonConvert.class)
获取 PersonConvert 实例,只是将实例放在里面,是约定俗成。
# 映射前置 & 后置
和拦截器类似,MapStruct 在执行映射前后,都有一次前置和后置的回调,所以我们可以在执行映射前或者后,对映射的两个类进行特殊处理。
@BeforeMapping
注解作用于方法上,注解标注的方法会在映射方法中首先执行。
@AfterMapping
注解同样作用于方法之上,标记要在生成的映射方法的末尾调用的方法,就 在映射方法的最后一个返回语句之前。
可以同时用 @BeforeMapping
注解标注多个方法,也可以同时用 @AfterMapping
注解标注多个方法。在这种情况下,在生成的实现类中,这些方法会按照我们在接口或者抽象类中定义的顺序执行。
该方法(两个注解指定的方法)既可以在抽象映射器类中实现,也可以通过 @Mapper
注解的 uses 属性指定的类型(类或接口)声明,还可以在用 @Context
参数的类型中实现,以便在映射方法中使用。
PersonDTO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class PersonDTO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
PersonVO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private Integer id;
private String personName;
private Integer personAge;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
13
PersonConvert 类
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@BeforeMapping
default void beforeMapping(PersonDTO personDTO) {
System.out.println("beforeMapping:" + personDTO);
}
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO personVOToDTO(PersonDTO personDTO);
@AfterMapping
default void afterMapping(PersonDTO personDTO, @MappingTarget PersonVO personVO) {
System.out.println("afterMapping - personDTO:" + personDTO);
System.out.println("afterMapping - personVO:" + personVO);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@BeforeMapping
修饰的方法只有 Source 类,因为还没有进行映射,所以 Target 类是 null。
@AfterMapping
参数为了区分哪个是 Source 类和 Target 类,则需要用 @MappingTarget
标明 Target 类。
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setId(127);
personDTO.setName("可乐");
personDTO.setAge(24);
personDTO.setAddress("深圳");
personDTO.setBirthDate(new Date());
personDTO.setIsMarried(true);
PersonVO personVO = PersonConvert.INSTANCE.personVOToDTO(personDTO);
System.out.println("转换成功的 personVO:" + personVO);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
打印:
beforeMapping:PersonDTO(id=127, name=可乐, age=24, address=深圳, birthDate=Wed Jul 12 00:34:38 CST 2023, isMarried=true)
afterMapping - personDTO:PersonDTO(id=127, name=可乐, age=24, address=深圳, birthDate=Wed Jul 12 00:34:38 CST 2023, isMarried=true)
afterMapping - personVO:PersonVO(id=127, personName=可乐, personAge=24, address=深圳, birthDate=Wed Jul 12 00:34:38 CST 2023, isMarried=true)
转换成功的 personVO:PersonVO(id=127, personName=可乐, personAge=24, address=深圳, birthDate=Wed Jul 12 00:34:38 CST 2023, isMarried=true)
2
3
4
@AfterMapping
的使用场景比 @BeforeMapping
更多一下,如我们有些属性是经过其他属性计算而得出的,所以可以统一在 @AfterMapping
进行计算,因为此时其他属性都已经被注入了。
比如当年龄大于 24 后。则默认已经结婚:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO personVOToDTO(PersonDTO personDTO);
@AfterMapping
default void afterMapping(PersonDTO personDTO, @MappingTarget PersonVO personVO) {
// 当年龄大于 24 后。则默认已经结婚
if(personVO.getPersonAge() == 24) {
personVO.setIsMarried(true);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# uses 使用
如果我们有很多 MapStruct 的转换类,其中有部分的逻辑是重复的,那么我们可以将重复的逻辑封装到一个公共的类,然后通过 uses 来引用这个公共的类。
如上面使用了 @BeforeMapping
和 @AfterMapping
,我们将这两个方法放到一个类
public class BeforeAfterMapping {
@BeforeMapping
public void beforeMapping(PersonDTO personDTO) {
System.out.println("beforeMapping:" + personDTO);
}
@AfterMapping
public void afterMapping(PersonDTO personDTO, @MappingTarget PersonVO personVO) {
System.out.println("afterMapping - personDTO:" + personDTO);
System.out.println("afterMapping - personVO:" + personVO);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
然后 uses 引用
@Mapper(uses = {BeforeAfterMapping.class})
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO personVOToDTO(PersonDTO personDTO);
}
2
3
4
5
6
7
8
9
uses 支持多个类,逗号隔开。
uses 功能非常强大,我们将重复的逻辑在外面封装,然后使用 uses 引入进来,这对代码的阅读性和维护性都有很大的帮助。
# 引用类属性映射
# Mapping 映射
当一个实体类引用了另一个实体类,MapStruct 也能进行映射。
PersonDTO 类和引用的 SubPersonDTO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
private SubPersonDTO subPersonDTO;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class SubPersonDTO {
private Integer id;
private String dtoName;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PersonVO 类和引用的 SubPersonVO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private Integer id;
private String personName;
private Integer personAge;
private String address;
private Date birthDate;
private Boolean isMarried;
private SubPersonVO subPersonVO;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class SubPersonVO {
private Integer id;
private String voName;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
然后将 PersonDTO 的属性值注入到 PersonVO 里,包括引用的类。
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
@Mapping(source = "subPersonDTO", target = "subPersonVO")
PersonVO personVOToDTO(PersonDTO personDTO);
@Mapping(source = "dtoName", target = "voName")
SubPersonVO subPersonVOToDTO(SubPersonDTO subPersonDTO);
}
2
3
4
5
6
7
8
9
10
11
12
13
MapStruct 如果检测到是引用类型,则会在当前接口/类找引用类型的 Mapping 方法,如果有则进行对应的转换,没有则为 null。
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setId(127);
personDTO.setName("可乐");
personDTO.setAge(24);
personDTO.setAddress("深圳");
personDTO.setBirthDate(new Date());
personDTO.setIsMarried(true);
personDTO.setSubPersonDTO(createSubPersonDTO());
PersonVO personVO = PersonConvert.INSTANCE.personVOToDTO(personDTO);
System.out.println("转换成功的 personVO:" + personVO.getSubPersonVO());
}
public static SubPersonDTO createSubPersonDTO() {
SubPersonDTO subPersonDTO = new SubPersonDTO();
subPersonDTO.setId(10);
subPersonDTO.setDtoName("冰糖");
return subPersonDTO;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
打印:
转换成功的 personVO:SubPersonVO(id=10, voName=冰糖)
# 自定义映射规则
基于上面例子,SubPersonDTO 映射到 SubPersonVO,可以直接使用 @Mapping
进行 dtoName 到 voName 的映射,当涉及到复杂的逻辑,我们可以自定义映射。
还是使用上面的例子,我们只需要写具体的方法实现逻辑即可:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
@Mapping(source = "subPersonDTO", target = "subPersonVO")
PersonVO personVOToDTO(PersonDTO personDTO);
default SubPersonVO subPersonVOToDTO(SubPersonDTO subPersonDTO) {
SubPersonVO subPersonVO = new SubPersonVO();
subPersonVO.setId(subPersonDTO.getId());
subPersonVO.setVoName("雪梨");
return subPersonVO;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
和下面对比
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
@Mapping(source = "subPersonDTO", target = "subPersonVO")
PersonVO personVOToDTO(PersonDTO personDTO);
@Mapping(source = "dtoName", target = "voName")
SubPersonVO subPersonVOToDTO(SubPersonDTO subPersonDTO);
}
2
3
4
5
6
7
8
9
10
11
12
我们实现了自定义映射,不使用 MapStruct 的规则,我们指定 subPersonVO 的 voName 为雪梨。
所以打印:
转换成功的 personVO:SubPersonVO(id=10, voName=雪梨)
# 多级嵌套映射
当类 A 引用了类 B,类 B 引用了类 C,那么在映射的过程,依然可以用上面两个规则来映射,只不过多加了一个类 B 到类 C 的映射方法。
# 多转一
我们在实际的业务中少不了将多个对象转换成一个的场景。MapStruct 当然也支持多转一的操作。
如我们将 PersonDTO 和 PersonVO 的属性映射到 Person。
PersonDTO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
13
PersonVO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private Integer id;
private String name;
private Integer sge;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
13
Person 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Person {
private Integer id;
private String personDTOName;
private Integer personDTOAge;
private String personVOName;
private Integer personVOAge;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
personDTOName 和 personDTOAge 是 PersonDTO 的属性映射,personVOName 和 personVOAge 是 PersonVO 的属性映射。
但是 id、address、birthDate、isMarried 是 DTO 还是 VO 的属性映射呢?这需要我们手动去指定使用哪个类的属性。
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "personDTO.name", target = "personDTOName")
@Mapping(source = "personDTO.age", target = "personDTOAge")
@Mapping(source = "personVO.name", target = "personVOName")
@Mapping(source = "personVO.age", target = "personVOAge")
@Mapping(source = "personDTO.id", target = "id")
@Mapping(source = "personDTO.address", target = "address")
@Mapping(source = "personVO.birthDate", target = "birthDate")
@Mapping(source = "personVO.isMarried", target = "isMarried")
Person personVOToDTO(PersonVO personVO, PersonDTO personDTO);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
source 的 xxx.xx
,其中 xxx 就是对象名,xx 就是属性名。
public class Main {
public static void main(String[] args) {
Person person = PersonConvert.INSTANCE.personVOToDTO(createPersonVO(), createPersonDTO());
System.out.println("转换成功的 person:" + person);
}
public static PersonVO createPersonVO() {
PersonVO personVO = new PersonVO();
personVO.setId(199);
personVO.setName("冰糖");
personVO.setAge(20);
personVO.setAddress("深圳");
personVO.setBirthDate(new Date());
personVO.setIsMarried(false);
return personVO;
}
public static PersonDTO createPersonDTO() {
PersonDTO personDTO = new PersonDTO();
personDTO.setId(127);
personDTO.setName("可乐");
personDTO.setAge(24);
personDTO.setAddress("广西");
personDTO.setBirthDate(new Date());
personDTO.setIsMarried(true);
return personDTO;
}
}
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
打印:
转换成功的 person:Person(id=127, personDTOName=可乐, personDTOAge=24, personVOName=冰糖, personVOAge=20, address=广西, birthDate=Thu Jul 13 00:04:37 CST 2023, isMarried=false)
当然如果配合 @AfterMapping
,我们可以这样获取三个参数:
@AfterMapping
default void afterMapping(PersonVO personVO, PersonDTO personDTO, @MappingTarget Person person) {
}
2
3
4
# 更新 Bean 对象
有时候,我们不是想返回一个新的 Bean 对象,而是希望更新传入对象的一些属性。这个在实际的时候也会经常使用到。
更新 Bean 对象的方式和 @AfterMapping
修饰的方法一样,只不过 更新 Bean 对象需要我们手动调用方法,而 @AfterMapping
修饰的方法是 MapStruct 在映射后自动调用。
PersonDTO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
PersonVO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private Integer id;
private String personName;
private Integer personAge;
private String address;
private Date birthDate;
private Boolean isMarried;
}
2
3
4
5
6
7
8
9
10
11
12
13
将 PersonDTO 的部分属性注入到 PersonVO 里
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "age", target = "personAge")
@Mapping(target = "address", ignore = true)
void updatePersonVO(PersonDTO personDTO, @MappingTarget PersonVO personVO);
}
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
PersonVO personVO = createPersonVO();
PersonConvert.INSTANCE.updatePersonVO(createPersonDTO(), personVO);
System.out.println("更新成功的 PersonVO:" + personVO);
}
public static PersonVO createPersonVO() {
PersonVO personVO = new PersonVO();
personVO.setId(199);
personVO.setPersonName("冰糖");
personVO.setAddress("深圳");
return personVO;
}
public static PersonDTO createPersonDTO() {
PersonDTO personDTO = new PersonDTO();
personDTO.setAge(24);
personDTO.setBirthDate(new Date());
personDTO.setIsMarried(true);
return personDTO;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
打印:
更新成功的 PersonVO:PersonVO(id=null, personName=冰糖, personAge=24, address=深圳, birthDate=Thu Jul 13 00:18:29 CST 2023, isMarried=true)
MapStruct 会将 PersonDTO 的属性都更新到 PersonVO 里,而这个例子 PersonDTO 的 address 为 null,所以为了继续使用 PersonVO 的 address 深圳,则使用 ignore 参数无视 address 的映射,否则 address 会被映射为 null。
# 继承配置
# 正向继承
当我们一个 MapStruct 类有很多映射规则,并且很多映射规则,我们可以使用 @InheritConfiguration
来继承其他的映射规则,如:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO convert(PersonDTO personDTO);
@InheritConfiguration
void update(PersonDTO personDTO, PersonVO personVO);
}
2
3
4
5
6
7
8
9
10
11
12
这样 update 就继承了 convert 的配置,即等价于:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO convert(PersonDTO personDTO);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
void update(PersonDTO personDTO, PersonVO personVO);
}
2
3
4
5
6
7
8
9
10
11
12
13
但是当出现多个映射规则,我们就需要使用 @InheritConfiguration
的 name 属性来告诉 MapStruct 继承哪个方法的映射规则,如:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO convert(PersonDTO personDTO);
@Mapping(source = "name", target = "personName")
@Mapping(target = "address", ignore = true)
PersonVO convert2(PersonDTO personDTO);
@InheritConfiguration(name = "convert2")
void update(PersonDTO personDTO, PersonVO personVO);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这样 update 就是要 convert2 的映射规则,即等价于:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO convert(PersonDTO personDTO);
@Mapping(source = "name", target = "personName")
@Mapping(target = "address", ignore = true)
PersonVO convert2(PersonDTO personDTO);
@Mapping(source = "name", target = "personName")
@Mapping(target = "address", ignore = true)
void update(PersonDTO personDTO, PersonVO personVO);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 反向继承
我们可能会遇到一种情况就是,我们定义了一个方法将 DTO 转成 VO,这个方法写了很多的 @Mapping
映射规则,但是我们可能又需要将 VO 转为 DTO,此时也需要写很多的 @Mapping
,只是该注解的 source 和 target 进行互换,为了避免反向映射导致写重复的很多 @Mapping
,我们可以使用 @InheritInverseConfiguration
进行反向继承:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO convert(PersonDTO personDTO);
@InheritInverseConfiguration
PersonDTO inverseConvert(PersonVO personVO);
}
2
3
4
5
6
7
8
9
10
11
12
此时 inverseConvert 方法就使用了 convert 的规则,然后将规则进行反向编译,即等价于:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO convert(PersonDTO personDTO);
@Mapping(source = "personName", target = "name")
@Mapping(source = "personAge", target = "age")
PersonDTO inverseConvert(PersonVO personVO);
}
2
3
4
5
6
7
8
9
10
11
12
13
如果有很多映射规则的方法,则需要利用 @InheritInverseConfiguration
的 name 属性来告诉 MapStruct 反向继承哪个方法的映射规则,如:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO convert(PersonDTO personDTO);
@Mapping(source = "name", target = "personName")
PersonVO convert2(PersonDTO personDTO);
@InheritInverseConfiguration(name = "convert2")
PersonDTO inverseConvert2(PersonVO personVO);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这样 inverseConvert2
使用的规则就是方法 convert2 的映射规则,即等价于:
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "name", target = "personName")
@Mapping(source = "age", target = "personAge")
PersonVO convert(PersonDTO personDTO);
@Mapping(source = "name", target = "personName")
PersonVO convert2(PersonDTO personDTO);
@Mapping(source = "personName", target = "name")
PersonDTO inverseConvert2(PersonVO personVO);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 映射 format 规则
# 日期转换
如果我们需要进行日期的转换(Date 转成 String),可以使用 @Mapping
的 dateFormat 属性。
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Date birthDate;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private String birthDate;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
转换类使用 dateFormat 属性转成 yyyy-MM-dd HH:mm:ss
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "birthDate", target = "birthDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
PersonVO convert(PersonDTO personDTO);
}
2
3
4
5
6
7
8
运行类
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setBirthDate(new Date());
PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
System.out.println("转换成功的 personVO:" + personVO);
}
}
2
3
4
5
6
7
8
运行:
转换成功的 personVO:PersonVO(birthDate=2023-07-15 00:24:35)
# 小数点转换
如果我们需要进行 Double 或 Float 转成 String,可以使用 @Mapping
的 numberFormat 属性。
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Double num;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private String num;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
转换类使用 numberFormat 属性转成保留两位小数点
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "num", target = "num", numberFormat = "#.00")
PersonVO convert(PersonDTO personDTO);
}
2
3
4
5
6
7
8
运行类
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setNum(1.2345);
PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
System.out.println("转换成功的 personVO:" + personVO);
}
}
2
3
4
5
6
7
8
打印:
转换成功的 personVO:PersonVO(num=1.23)
# 依赖注入
# Spring 注入
如果搭配 Spring 使用,MapSturct 也支持使用依赖注入到 Spring 容器,同时也推荐使用依赖注入。
@Mapper(componentModel = "spring")
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
PersonVO personVOToDTO(PersonDTO personDTO);
}
2
3
4
5
6
7
添加了 @Mapper(componentModel = "spring")
,就等于添加了 @Mapper
+ @Component
。
# 依赖注入策略
MapStruct 默认是调用属性的 setter 和 getter 来获取和设置值,那么我们也可以指定通过有参构造器来实现设置值。
@Mapper(componentModel = "cdi", injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "birthDate", target = "birthDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
PersonVO personVOToDTO(PersonDTO personDTO);
}
2
3
4
5
6
7
8
# 自定义类型转换
有时候,在对象转换的时候可能会出现这样一个问题,就是源对象中的类型是 Boolean 类型,而目标对象类型是 String 类型,这种情况可以通过 @Mapper
的 uses 属性来实现:
PersonDTO 类:
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Boolean isMarried;
}
2
3
4
5
6
7
8
PersonVO 类
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private String isMarried;
}
2
3
4
5
6
7
8
PersonConvert 转换类,使用 uses 属性来指定不同类型的转换
@Mapper(uses = {BooleanStrFormat.class})
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
PersonVO personVOToDTO(PersonDTO personDTO);
}
2
3
4
5
6
7
BooleanStrFormat 类
public class BooleanStrFormat {
public String toStr(Boolean isMarried) {
return isMarried ? "已婚" : "未婚";
}
}
2
3
4
5
运行:
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setIsMarried(true);
PersonVO personVO = PersonConvert.INSTANCE.personVOToDTO(personDTO);
System.out.println("转换成功的 personVO:" + personVO);
}
}
2
3
4
5
6
7
8
打印:
转换成功的 personVO:PersonVO(isMarried=已婚)
可以知道 DTO 的 Boolean 经过 BooleanStrFormat 转换,得出了 VO 的已婚。
要注意的是,如果使用了例如像 Spring 这样的环境 @Mapper(componentModel = "spring")
,Mapper 用 uses 引入类实例的方式也必须是自动注入,即这个类也应该纳入 Spring 容器。
# 封装公共映射类
基于上面的使用,我们可以封装一个公共的映射类
public interface BaseMapperConvertor<S, T> {
T convert(S sourceClass);
S convertInvert(T targetClass);
List<T> convert(List<S> sourceClass);
List<S> convertInvert(List<T> targetClass);
void update(S sourceClass, @MappingTarget T targetClass);
void updateInvert(T targetClass, @MappingTarget S sourceClass);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这是最基本的封装类,仅限 Source 类和 Target 类的属性类型和属性名一致。
使用:
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
private Integer id;
private String name;
private Integer age;
private String address;
private Date birthDate;
private Boolean isMarried;
}
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
PersonDTO 和 PersonVO 的属性类型和属性名一样,则定义一个 MapStruct 类继承 BaseMapper
@Mapper
public interface PersonConvert extends BaseMapperConvertor<PersonDTO, PersonVO> {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
}
2
3
4
运行类:
public class Main {
public static void main(String[] args) {
PersonDTO personDTO = new PersonDTO();
personDTO.setId(127);
personDTO.setName("可乐");
personDTO.setAge(24);
personDTO.setAddress("深圳");
personDTO.setBirthDate(new Date());
personDTO.setIsMarried(true);
PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
System.out.println("转换成功的 personVO:" + personVO);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
打印:
转换成功的 personVO:PersonVO(id=127, name=可乐, age=24, address=深圳, birthDate=Fri Jul 14 00:20:20 CST 2023, isMarried=true)
我们可以封装更多的映射类,这是基于自己业务需求来封装,如有很多类需要将 name 转成 personName,则可以自行基于 BaseMapper 进一步封装。
# 遵循原则
- 当多个对象中, 有其中一个为 null, 则会直接返回 null
- 如一对一转换一样, 属性通过名字来自动匹配。因此, 名称和类型相同的不需要进行特殊处理
- 当多个原对象中,有相同名字的属性时,需要通过 @Mapping 注解来具体的指定, 以免出现歧义(不指定会报错)。如上面的 name
- 属性也可以直接从传入的参数来赋值,如自定义转换规则和
@AfterMapping
修饰的方法修改属性值