优质博文:IT-BLOG-CN
一、痛点
代码中存在很多Java Bean
之间的转换,编写映射转化代码是一个繁琐重复还易出错的工作。使用BeanUtils
工具时,对于字段名不一致和嵌套类型不一致时,需要手动编写。并且基于反射,对性能有一定开销。Spring
提供的BeanUtils
针对apache
的BeanUtils
做了很多优化,整体性能提升了不少,不过还是使用反射实现,针对复杂场景支持能力不足。
二、MapStruct 机制
MapStruct
是编译期动态生成getter/setter
,在运行期直接调用框架编译好的class
类实现实体映射。因此安全性高,编译通过之后,运行期间就不会报错。其次速度快,运行期间直接调用实现类,不会在运行期间使用发射进行转换。
三、环境搭建
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>
四、使用
单一对象转化
创建映射:如下两个类进行对象之间的转换
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
}
两者之间进行映射,需要创建一个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);
}
当我们需要将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();
}
}
多个对象转换为一个对象
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
}
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);
}
子对象映射
多数情况下,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
}
在修改StudentMapper
之前,我们先创建一个ClassInfoMapper
转换器:
@Mapper
public interface ClassInfoMapper {
ClassInfoMapper INSTANCE = Mappers.getMapper(ClassInfoMapper.class);
ClassInfoDto dto(ClassInfo classInfo);
}
创建完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);
}
我们看先编译后的代码:新增了一个新的映射方法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;
}
}
数据类型映射
自动类型转换适用于一下几种情况:
【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
}
创建映射器
@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);
}
List映射
定义一个新的映射方法
@Mapper
public interface StudentMapper {
List<StudentDto> map(List<Student> student);
}
自动生成的代码如下:
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;
}
}
Set
与Map
型数据的处理方式与List
相似:
@Mapper
public interface StudentMapper {
Set<StudentDto> setConvert(Set<Student> student);
Map<String, StudentDto> mapConvert(Map<String, Student> student);
}
添加默认值
@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);
}
如果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;
}
}
添加表达式
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);
}
五、依赖注入
如果你使用的是Spring
,只需要修改映射器配置,在Mapper
注解中添加componentModel = "spring"
,告诉MapStruct
在生成映射器实现类时,支持通过Spring
的依赖注入来创建,就不需要在接口中添加INSTANCE
字段了。这次生成的StudentMapperImpl
会带有@Component
注解,就可以在其它类中通过@Autowire
注解来使用它。
@Mapper(componentModel = "spring")
public interface StudentMapper {}
如果你不使用Spring
, MapStruct
也支持Java CDI
:
@Mapper(componentModel = "cdi")
public interface StudentMapper {}