简介
MapStruct
是一款基于编译期注解处理技术,用于对象之间的映射转换工具,如从DTO
映射为Entity
进行持久化,从Entity
映射为VO
进行页面渲染。
通常进行映射转换的2个对象大部分属性类型及属性名一致,或者完全一样。
在MapStruct
之前进行映射工作主要通过以下2种方式:
方式 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
BeanUtils | 代码量少 | 使用反射机制,存在性能问题 | 少量映射转换需求且无复杂转换逻辑 |
手动编码映射 | 灵活、可进行复杂逻辑转换 | 需要编写大量模板代码 | 少量映射转换需求且转换逻辑复杂、待映射转换的2个对象差异较大 |
MapStruct
主要解决以上方式的2个问题:
- 减少编码量、减少模板代码
- 提升性能的同时保持灵活性
可以这么说,需手动编码进行的映射转换都能通过MapStruct
搞定。但并不是说MapStruct
适合所有场景,在适合手动编码映射时,盲目引入MapStruct
反而会增加复杂性。
最适合使用MapStruct
的场景:Web
环境中,为保持领域对象独立性,存在与领域数据、行为相关的Command
、DTO
、VO
,需要大量的映射转换操作且彼此之间的属性都比较相似。
环境配置
MapStruct
依赖包含2部分:核心API依赖、注解处理器
- 在
Maven
环境下依赖配置如下:
...
<properties>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
...
<dependencies>
<!-- 定义MapStruct核心依赖 -->
<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.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<!-- 在编译插件中配置MapStruct注解处理器 -->
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
-
IDE
环境每个
IDE
中都有自身的代码语法树解析机制来进行代码快捷跳转、代码关系识别等。通过编译期注解处理器生成的代码无法被IDE
识别,在使用时会提示编译错误。不同
IDE
需要安装对应的插件或配置解决该问题。以IDEA
为例,需要开启Enable annotation processing
.
快速上手
Mapper定义
DTO
定义如下:
public class PersonDTO {
private String id;
private String userName;
// 省略getter/setter
Entity
定义如下:
public class Person {
private String id;
private String name;
// 省略getter/setter
DTO
与Entity
之间存在相同和不同名称的属性,这种属性间的差异符合大部分的情况。
下面定义Mapper
来处理这两者之间的映射:
@Mapper
public interface PersonMapper {
// 当前PersonMapper实例对象
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
@Mapping(source = "name", target = "userName")
PersonDTO toDTO(Person person);
分析一下上面多个定义的含义:
-
该
Mapper
为一个接口,通过接口定义一些行为,MapStruct
负责在编译期生成实现该接口的代码。可以使用一个抽象类代替。 -
@Mapper
: 该注解用于标识当前对象为一个映射转换器,不可缺省。 -
toDTO
方法:方法名无限制,MapStruct
负责实现该方法。 -
@Mapping
:对于Person
与PersonDTO
名称不一致的属性显式指定映射关系。 -
INSTANCE
:该Mapper
对应的实例,为保持单例,建议定义在接口内。
Mapper使用
在需要映射转换的位置调用PersonMapper.INSTANCE.toDTO(p)
原理浅析
MapStruct
使用编译期注解处理技术JSR-269:Pluggable Annotation Processing API。通过定义一系列注解,在编译时解析这些注解来生成对应Mapper
的实现类,真实的转换代码都在编译期间生成。
编译时MapStruct
注解处理器会查找被@Mapper
修饰的类,这也是为什么@Mapper
不可以缺省。
编译会进行多轮,如果在过程中产生新的文件,那么会继续下一轮编译。在IDE
开发环境下编译,可以观察到target
目录下的子目录generated-sources
包含了MapStruct
处理器生成的源代码、classes
目录下包含了再次编译的类文件。
MapStruct
生成的源代码:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-03-02T22:41:17+0800",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_151 (Oracle Corporation)"
)
public class PersonMapperImpl implements PersonMapper {
@Override
public PersonDTO toDTO(Person person) {
if ( person == null ) {
return null;
}
PersonDTO personDTO = new PersonDTO();
personDTO.setUserName( person.getName() );
personDTO.setId( person.getId() );
return personDTO;
}
}
MapStruct
为被@Mapper
修饰的接口生成了一个实现代码,实现的逻辑与手工编码方式基本无差异,使用原生的getter/setter
进行属性拷贝。
MapStruct
比BeanUtils
性能好的原因:使用原生的方法,把运行期的做的事情在编译期就做完了,避免通过反射查找带来的性能损耗。
基础使用
上面的示例展示了最简单的使用方法。
真实场景下,情况就变的很复杂,大体需要解答以下几个问题:
-
属性类型不一样怎样处理?
-
待映射转换的目标对象如何实例化?
-
如何处理枚举、集合、日期、内嵌引用类型?
-
存在复杂映射逻辑该怎样处理?
默认行为
MapStruct
默认实现了很多隐式转换,这些隐式转换让我们在大多数时候可以不进行显式定义映射逻辑即可完成需求。
以下这些类型之间会自动进行映射:
-
基本类型及其包装类型之间
如源属性为
int
类型,目标属性为Integer
类型,则未显式指定映射时,会自动映射。 -
基本类型(及其包装类型)与
String
如源属性为String类型,目标属性为
Integer
类型,则未显式指定映射时,会自动生成映射Integer.parseInt("2")
-
枚举类型与
String
类型之间 -
日期类型与
String
类型之间日期类型较为特殊,一般需要指定日期的格式。需要在映射方法上通过
@Mapping#dateFormat
显式指定日期格式,如@Mapping(source="birthday", target="birthday", dateFormat="yyyy-MM-dd")
集合映射
集合之间的映射与Bean
对象之间映射无太大区别,只是在这个基础上循环进行Bean
对象映射转换。
List<PersonDTO> toDTOs(List<Person> personList);
生成的代码对persons
进行遍历对每个Person
映射转换并放到一个新的集合中,以下为生成的代码片段:
@Override
public List<PersonDTO> toDTOs(List<Person> personList) {
if ( personList == null ) {
return null;
}
List<PersonDTO> list = new ArrayList<PersonDTO>( personList.size() );
for ( Person person : personList ) {
list.add( toDTO( person ) );
}
return list;
}
Map
的处理与集合一样。
可能这里会引申出另一个问题:源对象是个集合,目标对象是个普通Bean
(或者相反)该怎么处理? 这里只能使用后面会说过的自定义映射方法。
内嵌引用对象
源对象中包含一个引用对象的属性,MapStruct
会自动递归处理这种内嵌的引用对象属性。
但是显式指定映射配置时,需要通过层级属性名来指定内嵌对象的属性名。如Person
中包含一个address(Address)
对象属性,PersonDTO
中包含一个address(AddressDTO)
对象属性,通过@Mapping
显式指定属性映射:
// source=address.name 为Address中的属性名, target=address.addressName 为AddressDTO中的属性名
@Mapping(source="address.name", target="address.addressName")
PersonDTO toDTO(Person person);
平坦化内嵌对象
当目标对象中的属性为源对象的基本属性及内嵌对象的属性组合时,可以在@Mapping
中指定target=.
来时平坦化内嵌对象。
Person
定义如下:
public class Person {
private String id;
private String name;
private Address address;
// 省略setter/getter
}
Address
定义如下:
public class Address {
private String id;
private String city;
private String detail;
// 省略setter/getter
}
PersonDTO
定义如下:
public class PersonDTO {
private String id;
private String name;
private String city;
private String detail;
// 省略setter/getter
}
如上示例,PersonDTO
中包含了Person
部分基础属性以及Address
的属性。
一种方式在Mapper
中显式指定出需要映射的内嵌属性,这种方式在属性过多时,较为繁琐。MapStruct
提供了另一种较为简便的方式:平坦化内嵌对象。
@Mapping(source = "id", target = "id")
@Mapping(source = "address", target = ".")
// @Mapping(source = "address.city", target = "city")
// @Mapping(source = "address.detail", target = "detail")
PersonDTO toDTO(Person person);
使用@Mapping
中指定target=.
将源属性address
中的所有属性映射到当前目标对象PersonDTO
的直接属性上,无需对每个内嵌对象的属性显式指定映射关系。
需要注意的是:如果bean
的基础属性与内嵌对象的属性冲突时,需要显式指定映射关系。如Address
中存在id
属性,Person
中也存在id
属性,此时MapStruct
无法区分出哪个才是真正需要的。
自定义映射方法
以上都是通过一些内置的配置完成映射,MapStruct
还允许用户自定义方法来完成一些属性之间的复杂映射逻辑。
复用上例中的Person
和Address
,定义PersonDTO
:
public class PersonDTO {
private String id;
private String name;
private String address; // address 由Address中的city + detail拼接而成
// 省略setter/getter
}
实现需求:Address
中的city
与detail
属性值拼接成一个新的字符后才能作为PersonDTO
中的address
属性值。
PersonDTO toDTO(Person person);
default String addressToString(Address address) {
return address.getCity() + "-" + address.getDetail();
}
自定义了一个addressToString
方法用于拼接逻辑,MapStruct
在使用该方法返回的结果对address
属性的赋值。
MapStruct
是如何识别自定义的映射方法?
源属性的类型作为参数,目标属性的类型作为返回值即可作为该属性对映射的自定义方法。如上例中,需要把一个Address
类型的属性映射成一个String
类型的属性,与addressToString
方法定义一致。
在接口中定义默认方法是JDK8
才有的特性,如果JDK
版本较低时,可以将Mapper
定义为抽象类。
额外的问题
一些特定问题,需参考官网说明,此处抛砖引玉简单介绍一下:
-
除了在当前
Mapper
自定义方法,如果某类属性映射会在多个Mapper
中共享时也可以通过@Mapper#uses
指定一个外部的类专门进行自定义映射方法。 -
一对映射属性找到多个满足要求的方法时,默认行为会抛出异常。可以通过
@qualifiedBy
或qualifiedByName
精确指定方法
更新已有对象属性
上面所有示例都在说明从一个源对象映射转换成一个新对象。在一些场景下需要指定源对象来更新已有的对象属性而不是创建一个新的对象,如在web更新用户信息:前端传递需要修改的信息,后端根据id
查找在数据库中持久化的用户数据,将前端的数据更新到当前对象中。
MapStruct
通过@MappingTarget
来支持这种更新操作。
// 方法的参数返回类型不是必须,可以指定为void
Person toEntity(PersonDTO personDTO, @MappingTarget Person person);
@MappingTarget
用来指定待更新的目标对象。
查看一下生成的源代码:
@Override
public void toEntity(PersonDTO personDTO, Person person) {
if ( personDTO == null ) {
return;
}
person.setId( personDTO.getId() );
person.setName( personDTO.getName() );
}
Spring容器内使用
MapStruct
通过@Mapper#componentModel
可以在多种容器内使用。
非容器环境
非容器环境下,需要对Mapper
实例化并取得引用,而不应在每个使用的地方重新实例化一次。
@Mapper
public interface PersonMapper {
// 当前PersonMapper实例对象
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
@Mapping(source = "name", target = "userName")
PersonDTO toDTO(Person person);
}
Spring容器环境
@Mapper(componentModel="spring")
public interface PersonMapper {
@Mapping(source = "name", target = "userName")
PersonDTO toDTO(Person person);
}
通过@Mapper#componentModel
指定当前Mapper
托管在Spring容器
中,需要使用该Mapper
时,通过注入的方式引入即可。
@Resource
private PersonMapper mapper;
小结
本文简单介绍MapStruct
使用场景及一些日常中大概率会使用的特性。MapStruct
足够“聪明”,默认行为足以解决大部分问题,特别适合存在大量映射转换的场景,如Web
开发环境下。
整篇都基于几个假设:目标对象存在无参构造器;源对象存在getter
方法;目标对象存在setter
方法等。实际情况可能与此相反,这些在MapStruct
中都提供了对应的解决方案。