MapStruct使用

MapStruct

[TOC]

什么是MapStruct

业务系统开发过程中,常常有分层概念,如持久层,业务层,数据传输层,视图层。

每个分层各自有自己的bean命名规则,对应上面提到的,如DO,BO,DTO,VO等。虽然命名不同,但绝大部分情况下,bean的内容都大同小异。所以,相同业务对象Bean在不同层交换数据时常常会涉及到类型的映射转换。MapStruct就是其中一种。

与其他Bean映射工具对比

市面上有很多映射工具类,比较流行的有Spring BeanUtils,Cglib BeanCopier,Apache BeanUtils,Dozer,orika,以及这次讲的MapStruct。

对上面提到的框架进行归类:

框架 深拷贝/浅拷贝 实现方式 执行阶段
Spring BeanUtils 浅拷贝 Java反射 运行期
Cglib BeanCopier 浅拷贝 修改字节码生成代理类,代理类实现get、set 运行期
Apache BeanUtils 浅拷贝 Java反射 运行期
Dozer 深拷贝 Java反射 运行期
orika 深拷贝 javassist生成字节码 编译期
MapStruct 深拷贝 Annotation Processor生成源码 编译期

性能对比:

就通常使用上来说,性能由高到底为:MapStruct > orika > Cglib BeanCopier > Spring BeanUtils > Apache BeanUtils > Dozer

MapStruct优势在于功能丰富,编译期生成Java源码进行映射,性能高。

具体对比结果网上有很多,不细说。

使用方式

配置依赖

maven方式

基本配置
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
...
<properties>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
...
<dependencies>
<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>
<!-- MapStruct依靠Annotation Processor生成代码 -->
<!-- maven import方式无法统一管理plugin版本,仍需手动定义,-->
<!-- mapstruct-processor版本一般与mapstruct版本保持一致 -->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
...
集成Spring

如果是Spring项目,建议配置一下,可以将MapStruct生成的Mapper类直接依赖注入到其他Bean。

主要在编译插件配置中加上一些配置:

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
<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-processor.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>
-Amapstruct.suppressGeneratorTimestamp=true
</arg>
<!-- spring项目可以在这里全局配置mapper的defaultComponentModel为spring -->
<!-- mapper会成为受spring管理的bean,被依赖注入至其他bean -->
<arg>
-Amapstruct.defaultComponentModel=spring
</arg>
</compilerArgs>
</configuration>
</plugin>
集成lombok

一般业务项目中,Lombok也是常用的一款工具,需要注意的是,Lombok也依赖于Annotation Processor,所以也需要配置Lombok的Annotation Processor Paths,编译时才能进行代码生成。

注意:Lombok 在1.18.16版本中引入了一项重大改动changelog,必须要再额外添加一个Annotation Processor:lombok-mapstruct-binding。见如下配置:

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
49
50
51
52
53
54
<properties>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.16</org.projectlombok.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</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>${org.projectlombok.version}</version>
<scope>provided</scope>
</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>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>

<!-- 如果Lombok是1.18.16以上版本,这个注解处理器一定要加 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.1.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

映射器 Mapper

定义映射器

MapStruct使用@Mapper来定义转换类。

1
2
3
4
5
@Mapper
public interface VenueTransformer {
VenueDTO toVenueDTO(VenueDO venueDO);
List<VenueDTO> toVenueDTOList(List<VenueDO> venueDOList);
}

定义这样一个接口(也可以是抽象类),在编译时,MapStruct将根据参数与返回类型,为其中字段自动生成转换代码。

以下是自动生成的代码,会在target/generate-sources目录下

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
// ...
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_131 (Oracle Corporation)"
)
@Component
public class VenueTransformerImpl implements VenueTransformer {

@Override
public VenueDTO toVenueDTO(VenueDO venueDO) {
if ( venueDO == null ) {
return null;
}
VenueDTO venueDTO = new VenueDTO();
venueDTO.setAdderName( venueDO.getAdderName() );
venueDTO.setAdderNo( venueDO.getAdderNo() );
venueDTO.setAddTime( venueDO.getAddTime() );
venueDTO.setUpdaterName( venueDO.getUpdaterName() );
venueDTO.setUpdaterNo( venueDO.getUpdaterNo() );
venueDTO.setUpdateTime( venueDO.getUpdateTime() );
venueDTO.setVenueId( venueDO.getVenueId() );
venueDTO.setVenueName( venueDO.getVenueName() );
return venueDTO;
}

@Override
public List<VenueDTO> toVenueDTOList(List<VenueDO> venueDOList) {
if ( venueDOList == null ) {
return null;
}
List<VenueDTO> list = new ArrayList<VenueDTO>( venueDOList.size() );
for ( VenueDO venueDO : venueDOList ) {
list.add( toVenueDTO( venueDO ) );
}
return list;
}
}

可以发现,源类型与目标类型中的字段名如果一样,MapStruct会自动映射上。单个Bean以及Bean List都会生成,拓展开也可以是Set,又或者是一个具体的实现类,如ArrayList等。

获取映射器

如果如前文所述,在maven配置时,配置了defaultComponentModel=spring,那么可以直接像注入其他@Service一样注入这个@Mapper即可。

如果没有配置,也可以通过Mappers.getMapper( VenueTransformer.class )来获取映射器实例。

定义字段映射规则

如果源类型与目标类型中字段名不一样,我们也可以通过@Mapping注解来自定义字段的映射关系。

1
2
3
4
5
@Mapper
public interface ExhibitionTransformer {
@Mapping(source = "exhibitionName", target = "exhibitionShowName")
ExhibitionDTO toExhibitionDTO(ExhibitionDO exhibitionDO);
}

此处会将ExhibitionDO.exhibitionName映射到ExhibitionDTO.exhibitionShowName上。

MapStruct会自动处理一些类型转换,比如一个Integer类型要映射到String类型上,则会调用String.valueOf(int),具体见隐式类型转换

自定义Bean映射规则

如果映射过程比较复杂,定制性很高,MapStruct也提供了自己写代码的转换方式。只需要在接口里写个default方法即可。

消除歧义

返回值类型相同

如果一个Mapper(包括@Mapper.use()的其他Mapper)中,出现了多个方法,他们的入参类型相同且返回值类型相同,会出现歧义,MapStruct会不知道要用哪一个具体的转换器方法。此时,我们可以通过给方法”起名字”的方式来消除歧义性,给方法加上@Named注解。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Mapper(imports = {EncryptUtil.class, CompanyLevel.class})
public interface CompanyTransformer {
CompanyDTO toCompanyDTO(CompanyDO companyDO);
/**
* 需要添加@Named,否则会与toCompanyDTO冲突。为什么?
* 加入另一个Mapper引用了当前mapper,转换时遇到了需要将CompanyDO转换为CompanyDTO,
* 那么将出现歧义,这时就需要通过@Named将其区分,然后调用Mapping时通过qualifiedByName指定具体调用。
*/
@Named("customToCompanyDTO")
default CompanyDTO customToCompanyDTO(CompanyDO companyDO) {
if (companyDO == null) {
return null;
}
CompanyDTO companyDTO = new CompanyDTO();
companyDTO.setEncodeComId(EncryptUtil.encrypt(companyDO.getComId()));
companyDTO.setComShowName("公司名:" + companyDO.getComName());
companyDTO.setAddress(companyDO.getAddress());
companyDTO.setCompanyLevel(CompanyLevel.determineByValue(companyDO.getLevel()));
companyDTO.setStatus(companyDO.getStatus());
return companyDTO;
}
}
返回值类型存在继承关系

譬如有两个返回类型B,C,都继承于类型A,那么如果在同一个Mapper中定义返回值类型为A,那么会出现歧义,我们实际需要B还是C?

这时候可以通过@BeanMapping.resultType来加以区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Mapper( uses = MyFactory.class )
public interface MyMapper {
@BeanMapping( resultType = B.class )
A map( Source source );
}

public class MyFactory {
public B createB() {
return new B();
}
public C createC() {
return new C();
}
}

将多个不同类型Bean合并为一个其他类型的Bean

@Mapping注解中,source以及target均可以通过”obj.field”的方式指代深层的成员字段。通过这种方式,可以指定多个源类型中的部分字段,来合并成一个新的类型对象。

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
@Data
public class CompanyDTO extends BaseDTO {
private String encodeComId;
private String comShowName;
private String address;
private CompanyLevel companyLevel;
private Short status;
private Integer deleteFlag;
}
@Data
public class Exhibits {
private String exhibitsName;
private String producerName;
private CompanyDTO producerDetail;
private String authorName;
private CompanyEmployee authorDetail;
}
@Data
public class CompanyEmployee extends EmployeeDO {
private String departmentName;
}
@Data
public class EmployeeDO extends BaseDO {
private String empNo;
private String empName;
}

@Mapper
public interface ExhibitsTransformer {
@Mapping(source = "exhibitsName", target = "exhibitsName")
@Mapping(source = "companyDTO.comShowName", target = "producerName")
@Mapping(source = "companyDTO", target = "producerDetail")
@Mapping(source = "companyEmployee.empName", target = "authorName")
@Mapping(source = "companyEmployee", target = "authorDetail")
Exhibits buildExhibits(String exhibitsName, CompanyDTO companyDTO, CompanyEmployee companyEmployee);
}

通过一个Bean更新另一个Bean的内容

这里会用到一个@MappingTarget,指向需要被更新的Bean类型。

1
2
@Mapping(source = "companyDTO.comShowName", target = "producerName")
void updateProducerInfo(@MappingTarget Exhibits exhibits, CompanyDTO companyDTO);

这里也可以返回Exhibits类型,可以配合其他映射方法实现链式转换。

调用其他映射器

可以通过@Mapper(use=AnotherTransformer.class)来将另一个Mapper引入到当前Mapper来,这样做的好处是,其他Mapper写的类型转换可以实现复用。比如,其他Mapper定义了一个Date到String的类型转换,我这个Mapper中涉及到的Date到String也需要如此转换,那么就可以直接把对应的Mapper引入进来即可。

1
2
3
4
5
public class DateMapper {
public String asString(Date date) {
return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).format( date ) : null;
}
}

引入DateMapper

1
2
3
4
@Mapper(uses=DateMapper.class)
public interface CarMapper {
CarDto carToCarDto(Car car);
}

映射Map类型

有时会涉及到Map之间的相互转换,譬如Map<String, String>转换为Map<Long, Date>,可以使用@MapMapping注解:

1
2
@MapMapping(valueDateFormat = "yyyy-MM-dd")
Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> sourceMap);

映射枚举类型

可以使用MapStruct将一种枚举类型映射为另一种枚举类型。默认情况是根据枚举常量名进行映射,如果需要自定义,可以使用下面的方式:

1
2
3
4
5
6
7
8
9
@Mapper
public interface OrderMapper {
@ValueMappings({
@ValueMapping(source = "EXTRA", target = "SPECIAL"),
@ValueMapping(source = "STANDARD", target = "DEFAULT"),
@ValueMapping(source = "NORMAL", target = "DEFAULT")
})
ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}

如上会将OrderType.EXTRA映射到ExternalOrderType.SPECIAL

高级配置项

默认值

使用@Mapping注解中的defaultValue参数,可以配置映射项的默认值,如果源值为null,那么就会取这个defaultValue的值作为目标值。

如果目标字段是一个新的字段,只需要给上默认常量呢?可以使用constant参数即可。

1
2
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
@Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
表达式

常见的如日期格式化,MapStruct提供了Mapping.dateFormat,也可以自定义其他的format方式。

一种方式是单独写一个工具类,添加方法并引入当前Mapper中(见“调用其他映射器”章节DateMapper使用),我们也可以直接在@Mapping中定义特定的表达式,譬如:

1
2
3
4
5
6
@Mapper
public interface SourceTargetMapper {
@Mapping(target = "timeAndFormat",
expression = "java(new org.sample.TimeAndFormat(s.getTime(), s.getFormat()))")
Target sourceToTarget(Source s);
}

配合@imports注解,在使用表达式时,可以不使用全限定名称。

1
2
3
4
5
6
7
8
imports org.sample.TimeAndFormat;

@Mapper( imports = TimeAndFormat.class )
public interface SourceTargetMapper {
@Mapping(target = "timeAndFormat",
expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);
}

写在最后

该文例举了MapStruct的配置方式,以及一些最为常见的用法,还有很多其他高级功能没有提及,具体请看官方文档:MapStruct 1.4.2.Final Reference Guide