# 一、痛点
代码中存在很多Java Bean
之间的转换,编写映射转化代码是一个繁琐重复还易出错的工作。使用BeanUtils
工具时,对于字段名不一致和嵌套类型不一致时,需要手动编写。并且基于反射,对性能有一定开销。Spring
提供的BeanUtils
针对apache
的BeanUtils
做了很多优化,整体性能提升了不少,不过还是使用反射实现,针对复杂场景支持能力不足。
# 二、MapStruct 机制
MapStruct
是编译期动态生成getter/setter
,在运行期直接调用框架编译好的class
类实现实体映射。因此安全性高,编译通过之后,运行期间就不会报错。其次速度快,运行期间直接调用实现类,不会在运行期间使用发射进行转换。

上图的流程可以概括为下面几个步骤:
【1】生成抽象语法树。Java
编译器对Java
源码进行编译,生成抽象语法树Abstract Syntax Tree,AST
。
【2】调用实现了JSR 269 API
的程序。只要程序实现了JSR 269 API
,就会在编译期间调用实现的注解处理器。
【3】修改抽象语法树。在实现JSR 269 API
的程序中,可以修改抽象语法树,插入自己的实现逻辑。
【4】生成字节码。修改完抽象语法树后,Java
编译器会生成修改后的抽象语法树对应的字节码文件。
# 三、环境搭建
Maven
依赖导入:mapstruct
依赖会导入MapStruct
的核心注解。由于MapStruct
在编译时工作,因此需要在<build>
标签中添加插件maven-compiler-plugin
,并在其配置中添加annotationProcessorPaths
,该插件会在构建时生成对应的代码。
<properties>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
<lombok.version>1.18.12</lombok.version>
</properties>
<dependencies>
<dependency>
<groupid>org.mapstruct</groupid>
<artifactid>mapstruct</artifactid>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupid>org.projectlombok</groupid>
<artifactid>lombok</artifactid>
<version>${lombok.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>
<!--下面这个 项目中不使用 Lombok的话 不用加-->
<path>
<groupid>org.projectlombok</groupid>
<artifactid>lombok</artifactid>
<version>${lombok.version}</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
32
33
34
35
36
37
38
39
40
41
42
43
44
# 四、使用
# 单一对象转化
创建映射:如下两个类进行对象之间的转换
public class Student {
private int id;
private String name;
// 两个类中存在不同的属性名,需要在Mapper接口中设置source和target
private String book;
// getters and setters or builder
}
public class StudentDto {
private int id;
private String name;
// 两个类中存在不同的属性名,需要在Mapper接口中设置source和target
private String letter;
// getters and setters or builder
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
两者之间进行映射,需要创建一个StudentMapper
接口并使用@Mapper
注解,MapStruct
就知道这是两个类之间的映射器。
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
// 两个类中存在不同的属性名,需要在Mapper接口中设置source和target
@Mapping(source = "student.book", target = "letter")
StudentDto toDto(Student student);
}
2
3
4
5
6
7
8
当我们需要将Student
属性映射到StudentDto
时
StudentDto studentDto = StudentMapper.INSTANCE.toDto(student);
当我们构建/编译应用程序时,MapStruct
注解处理器插件会识别出StudentMapper
接口并生成StudentMapperImpl
实体类:如果类型中包含Builder
, MapStruct
会尝试使用它来构建实例,如果没有MapStruct
将通过new
关键字进行实例化。
public class StudentMapperImpl implements StudentMapper {
@Override
public StudentDto toDto(Student student) {
if ( student == null ) {
return null;
}
StudentDtoBuilder studentDto = StudentDto.builder();
studentDto.id(student.getId());
studentDto.name(student.getName());
// ....
return studentDto.build();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 多个对象转换为一个对象
public class Student {
private int id;
private String name;
// getters and setters or builder
}
public class StudentDto {
private int id;
private int classId;
private String name;
// getters and setters or builder
}
public class ClassInfo {
private int id;
private int classId;
private String className;
// getters and setters or builder
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
StudentMapper
接口更新如下:如果两个属性中包含相同的字段时,需要通过source
和target
指定具体使用哪个类的属性。
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
@Mapping(source = "student.id", target = "id")
StudentDto toDto(Student student, ClassInfo classInfo);
}
2
3
4
5
6
7
# 子对象映射
多数情况下,POJO
中不会只包含基本数据类型,其中往往会包含其它类。比如说,一个Student
类中包含ClassInfo
类:
public class Student {
private int id;
private String name;
private ClassInfo classInfo;
// getters and setters or builder
}
public class StudentDto {
private int id;
private String name;
private ClassInfoDto classInfoDto;
// getters and setters or builder
}
public class ClassInfo {
private int classId;
private String className;
// getters and setters or builder
}
public class ClassInfoDto {
private int classId;
private String className;
// getters and setters or builder
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在修改StudentMapper
之前,我们先创建一个ClassInfoMapper
转换器:
@Mapper
public interface ClassInfoMapper {
ClassInfoMapper INSTANCE = Mappers.getMapper(ClassInfoMapper.class);
ClassInfoDto dto(ClassInfo classInfo);
}
2
3
4
5
创建完ClassInfoMapper
之后,我们再修改StudentMapper
:添加uses
标识,这样StudentMapper
就能够使用ClassInfoMapper
映射器
@Mapper(uses = {ClassInfoMapper.class})
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
@Mapping(source="student.classInfo", target="classInfoDto")
StudentDto toDto(Student student, ClassInfo classInfo);
}
2
3
4
5
6
7
我们看先编译后的代码:新增了一个新的映射方法classInfoDtoToclassInfo
。这个方法如果没有显示定义的情况下生成,因为我们将ClassInfoMapper
对象添加到了StudenMapper
中。
public class StudentMapperImpl implements StudentMapper {
private final ClassInfoMapper classInfoMapper = Mappers.getMapper( ClassInfoMapper.class );
@Override
public StudentDto toDto(Student student) {
if ( student == null ) {
return null;
}
StudentDtoBuilder studentDto = StudentDto.builder();
studentDto.id(student.getId());
studentDto.name(student.getName());
studentDto.classInfo = (classInfoDtoToclassInfo(student.calssInfo))
// ....
return studentDto.build();
}
protected ClassInfoDto classInfoDtoToclassInfo(ClassInfo classInfo) {
if ( classInfo == null ) {
return null;
}
ClassInfoDto classInfoDto = classInfoMapper.toDto(classInfo);
return classInfoDto;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 数据类型映射
自动类型转换适用于一下几种情况:
【1】基本类型及其包装类之间的转换:int
和Integer
,float
与Float
,long
与Long
,boolean
与Boolean
等。
【2】任意基本类型与任意包装类之间。如int
和long
,byte
和Integer
等。
【3】所有基本类型及包装类与String
之间。如boolean
和String
,Integer
和String
等。
【4】枚举和String
之间。
【5】Java
大数类型java.math.BigInteger, java.math.BigDecimal
和Java
基本类型(包括其包装类)与String
之间。
日期转换:指定格式
public class Student {
private int id;
private String name;
private LocalDate birth;
// getters and setters or builder
}
public class StudentDto {
private int id;
private String name;
pprivate String birth;
// getters and setters or builder
}
2
3
4
5
6
7
8
9
10
11
12
13
创建映射器
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
// 也可以指定数字的格式
// @Mapping(source = "price", target = "price", numberFormat = "$#.00")
@Mapping(source = "birth", target = "birth", dataFormat = "dd/MM/yyyy")
StudentDto toDto(Student student, ClassInfo classInfo);
}
2
3
4
5
6
7
8
9
# List映射
定义一个新的映射方法
@Mapper
public interface StudentMapper {
@Mappings({
@Mapping(source = "student.name", target = "mingzi"),
@Mapping(source = "student.age", target = "nianling")
})
StudentDto toStudentDto(Student student)
List<StudentDto> map(List<Student> students);
}
2
3
4
5
6
7
8
9
自动生成的代码如下:
public class StudentMapperImpl implements StudentMapper {
@Override
public List<StudentDto> map(List<Student> student) {
if ( student == null ) {
return null;
}
List<StudentDto> list = new ArrayList<StudentDto>( student.size() );
for ( Student student1 : student ) {
list.add( studentToStudentDto( student1 ) );
}
return list;
}
protected StudentDto studentToStudentDto(Student student) {
if ( student == null ) {
return null;
}
StudentDto studentDto = new StudentDto();
studentDto.setId( student.getId() );
studentDto.setName( student.getName() );
return studentDto;
}
}
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
Set
与Map
型数据的处理方式与List
相似:
@Mapper
public interface StudentMapper {
Set<StudentDto> setConvert(Set<Student> student);
// 可以使用@MapMapping实现对key和value的分别映射
Map<String, StudentDto> mapConvert(Map<String, Student> student);
}
2
3
4
5
6
7
# 添加默认值
@Mapping
注解有两个很实用的标志就是常量constant
和默认值defaultValue
。无论source
如何取值,都将始终使用常量值,如果source
取值为null
,则会使用默认值。修改一下StudentMapper
,添加一个constant
和一个defaultValue
:
@Mapper(componentModel = "spring")
public interface StudentMapper {
@Mapping(target = "id", constant = "-1")
@Mapping(source = "student.name", target = "name", defaultValue = "zzx")
StudentDto toDto(Student student);
}
2
3
4
5
6
如果name
不可用,我们会替换为zzx
字符串,此外,我们将id
硬编码为-1
。
@Component
public class StudentMapperImpl implements StudentMapper {
@Override
public StudentDto toDto(Student student) {
if (student == null) {
return null;
}
StudentDto studentDto = new StudentDto();
if (student.getName() != null) {
studentDto.setName(student.getName());
}
else {
studentDto.setName("zzx");
}
studentDto.setId(-1);
return studentDto;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 添加表达式
MapStruct
甚至允许在@Mapping
注解中输入Java
表达式。你可以设置defaultExpression
:
@Mapper(componentModel = "spring", imports = {LocalDateTime.class, UUID.class})
public interface StudentMapper {
@Mapping(target = "id", expression = "java(UUID.randomUUID().toString())")
@Mapping(source = "student.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())")
StudentDto toDtoWithExpression(Student student);
}
2
3
4
5
6
7
# BeforeMapping 和 @AfterMapping
为了进一步控制和定制化,我们可以定义@BeforeMapping
和@AfterMapping
方法。显然,这两个方法是在每次映射之前和之后执行的。在最终的实现代码中,会在两个对象真正映射之前和之后添加并执行这两个方法。
@Mapper(componentModel = "spring", imports = {LocalDateTime.class, UUID.class})
public interface StudentMapper {
@BeforeMapping
protected void validate(Student student) {
if(student.getPatientList() == null){
student.setPatientList(new ArrayList<>());
}
}
@AfterMapping
protected void updateResult(@MappingTarget StudentDto studentDto) {
studentDto.setName(studentDto.getName().toUpperCase());
studentDto.setDegree(studentDto.getDegree().toUpperCase());
}
@Mapping(target = "id", expression = "java(UUID.randomUUID().toString())")
@Mapping(source = "student.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())")
StudentDto toDtoWithExpression(Student student);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 五、依赖注入
如果你使用的是Spring
,只需要修改映射器配置,在Mapper
注解中添加componentModel = "spring"
,告诉MapStruct
在生成映射器实现类时,支持通过Spring
的依赖注入来创建,就不需要在接口中添加INSTANCE
字段了。这次生成的StudentMapperImpl
会带有@Component
注解,就可以在其它类中通过@Autowire
注解来使用它。同时还支持default
默认方式,使用工厂方式Mappers.getMapper(Class)
来获取;cdi
此时生成的映射器是一个应用程序范围的CDI bean
,使用@Inject
注解来获取;jsr330
生成的映射器用@javax.inject.Named
和@Singleton
注解,通过@Inject
来获取。
@Mapper(componentModel = "spring")
public interface StudentMapper {}
2
如果你不使用Spring
, MapStruct
也支持Java CDI
:
@Mapper(componentModel = "cdi")
public interface StudentMapper {}
2