MapStruct 使用介绍

  1. 1. 集成 MapStruct
  2. 2. 场景
    1. 2.1. 1. 正常的 Domain 到 Dto
      1. 2.1.1. 对象声明
      2. 2.1.2. 映射代码
    2. 2.2. 2. 存在不同名的字段属性
    3. 2.3. 3. 内嵌子类的映射
    4. 2.4. 4. 指定内嵌子类的属性映射
    5. 2.5. 5. 一个字段映射到多个字段
    6. 2.6. 6. 多字段到单字段
      1. 2.6.1. 6.1 使用 @Mapping 的 expression 属性
      2. 2.6.2. 6.2 使用 @AfterMapping 注解
      3. 2.6.3. 6.3 使用直接设置
    7. 2.7. 7. 两个对象到一个对象
    8. 2.8. 8. 列表类型到单个类型
    9. 2.9. 9. 字符串到日期类型
    10. 2.10. 10. 无 source 字段直接设置 target
  3. 3. 通用
    1. 3.1. 字符串数组转换为字符串
    2. 3.2. 传参配置 Mapper 方法
  4. 4. 总结

MapStruct 是一个 Bean 映射工具,通过配置的注解,可以生成 Java 代码进行 Bean 映射。

由于是通过两个 Bean 的信息生成 Java 代码,并通过调用属性的 Getter/Setter 方法来实现的功能,性能上比其它通过反射实现的 Bean 映射工具要高很多,相当于自己手写代码复制对象。

本文主要介绍 MapStruct 的几种对象映射场景,方便使用时选用。

集成 MapStruct

在 Gradle 中集成很简单,只用增加一个依赖和注解处理器配置既可。

1
2
3
4
dependencies {
compile "org.mapstruct:mapstruct:1.3.1.Final"
annotationProcessor "org.mapstruct:mapstruct-processor:1.3.1.Final"
}

场景

1. 正常的 Domain 到 Dto

对象声明
1
2
3
4
5
6
7
8
9
10
11
@Data
class Domain {
private String userId;
private String password;
}

@Data
class Dto {
private String userId;
private String password;
}
映射代码

MapStruct 默认会根据对象的字段名进行映射,如果存在不同名的字段会在编译的时候告警

1
2
3
4
5
6
private static DomainMapper mapper = Mappers.getMapper(DomainMapper.class);

@Mapper
interface DomainMapper {
Dto toDto(Domain domain);
}

将上面的 Dto 类的 password 字段改为 password2 重新编译可看到以下告警。

1
2
3
4
warning: Unmapped target property: "password2".
Dto toDto(Domain domain);
^
1 warning

MapStruct 是根据目标的类的属性来生成设置方法的,如果目标类存在,源类不存在则会产生以上告警。

2. 存在不同名的字段属性

对于不同名的映射,MapStruct 显然不知道怎么生成代码,所以我们需要使用 @Mapping 告诉 MapStruct 如何配置。 上面的 password2 映射需调整映射类代码如下。 source 表示 domain 中字段名,target 表示在 dto 中的字段名。

1
2
3
4
5
@Mapper
interface DomainMapper {
@Mapping(source = "password", target = "password2")
Dto toDto(Domain domain);
}

3. 内嵌子类的映射

新增以下两个类,并作为属性配置到类中。对于内嵌的属性,MapStruct 会生成一个方法来处理,处理的逻辑和外面是一样的,默认会自动处理同名属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SubDomain {
private String address;
}

class SubDto {
private String address;
}

class Domain {
// ...
private SubDomain address;
}

class Dto {
// ...
private SubDto address;
}

生成的映射类如下:

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
class Test$DomainMapperImpl implements DomainMapper {

@Override
public Dto toDto(Domain domain) {
if ( domain == null ) {
return null;
}

Dto dto = new Dto();

dto.setPassword2( domain.getPassword() );
dto.setUserId( domain.getUserId() );
dto.setName( domain.getName() );
dto.setAddress( subDomainToSubDto( domain.getAddress() ) );

return dto;
}

protected SubDto subDomainToSubDto(SubDomain subDomain) {
if ( subDomain == null ) {
return null;
}

SubDto subDto = new SubDto();

subDto.setAddress( subDomain.getAddress() );

return subDto;
}
}

4. 指定内嵌子类的属性映射

如果字类中不同属性名做映射,可以用 @Mapping 注解来配置。我们先在 SubDomainSubDto 中分别增加 addressCityaddressProvince 字段。

1
2
3
4
5
6
7
8
class SubDomain {
private String address;
private String addressCity;
}
class SubDto {
private String address;
private String addressProvince;
}

然后修改 Mapper 类为如下样式。通过 .(Dot) 来表示对应属性下的属性。

1
2
3
4
5
6
@Mapper
interface DomainMapper {
@Mapping(source = "password", target = "password2")
@Mapping(source = "address.addressCity", target = "address.addressProvince")
Dto toDto(Domain domain);
}

5. 一个字段映射到多个字段

假如一个字段是一个类或 Map 类型(address),而目标对象的一个属性是源类的该属性(address)的 address 值,另一属性是 addressCity 值。

修改上面介绍过的 SubDomainDto

1
2
3
4
5
6
7
8
9
10
11
12
13
class SubDomain {
private String address;
private String addressCity;
}

class Dto {
private String userId;
private String password2;
private String name;

private String address;
private String addressCity;
}

和第四节的方法类似,Mapper 接口定义如下

1
2
3
4
5
6
7
@Mapper
interface DomainMapper {
@Mapping(source = "password", target = "password2")
@Mapping(source = "address.address", target = "address")
@Mapping(source = "address.addressCity", target = "addressCity")
Dto toDto(Domain domain);
}

那可不可以直接 source 配置为 address,target 设置成不同的目标字段?当然可以,我们只要自己写 SubDomainaddress 属性的映射方法就可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Mapper
interface DomainMapper {
@Mapping(source = "password", target = "password2")
@Mapping(source = "address", target = "address", qualifiedByName = "subDomainAddr")
@Mapping(source = "address", target = "addressCity", qualifiedByName = "subDomainAddrCity")
Dto toDto(Domain domain);

@Named("subDomainAddr")
default String toDtoAddr(SubDomain domain) {
return domain.toString();
}

@Named("subDomainAddrCity")
default String toDtoAddrCity(SubDomain domain) {
return domain.toString();
}
}

MapStruct 是基于类型查找映射方法的,如果此处不用 qualified 的方式,MapStruct 不知道该选择 toDtoAddr 还是 toDtoAddrCity 来处理这里的字段映射,因为这两个方法都是传入 SubDomain 传出 String

6. 多字段到单字段

如果需要多个字段的内容合并到一个字段的类上,一般有三种方法

6.1 使用 @Mappingexpression 属性

expression 中可以写 Java 代码,可以通过 Java 代码进行组合

1
2
@Mapping( target = "address", expression = "java(dto.getAddressCity() + dto.getAddress())") 
Domain toDomain(Dto dto);
6.2 使用 @AfterMapping 注解

使用这种方法需要先使用 @Mapping 注解把 address 给忽略掉,不然可能会有报错(如果同名不同类型会报错,或者未映射告警)

1
2
3
4
5
6
7
@Mapping(target = "address", ignore = true)
Domain toDomain(Dto dto);

@AfterMapping
default void setAddress(@MappingTarget Domain domain, Dto dto) {
domain.setAddress(new SubDomain(dto.getAddress(), dto.getAddressCity()));
}
6.3 使用直接设置

如果源和目标的类型一样,不需要做特殊处理的话,使用直接设置的方法是比较建议的。

1
2
3
@Mapping(source = "addressDetail", target = "address.address")
@Mapping(source = "addressCity", target = "address.addressCity")
Domain toDomain(Dto dto);

7. 两个对象到一个对象

比如 C 对象是字段是 A 和 B 两个对象组合的,可以直接使用参数的变量名来配置映射。

1
2
3
4
5
6
@Mapper
interface DomainMapper {
@Mapping(source = "a.address", target = "address")
@Mapping(source = "b.name", target = "name")
C toDto(A a, B b);
}

8. 列表类型到单个类型

比如 List<String>String,这种是默认没有方法的,需要自己编写方法处理

1
2
3
4
5
6
@Mapper
interface DomainMapper {
default String toStr(List<String> strings) {
return String.join(",", strings);
}
}

9. 字符串到日期类型

MapStruct 本身内置了日期和字符串转换的功能,如果需要自定义日期格式可以在 @Mapping 注解的 dateFormat 属性中指定。

1
@Mapping(source = "date", target = "date", dateFormat = "yyyy-MM-dd HH:mm:ss")

10. 无 source 字段直接设置 target

  1. 使用 6.1 中讲的 expression 属性,给 target 直接设值。
  2. @Mappingconstant 属性,直接给字段设置值
  3. @MappingdefaultValue 属性给字段设置默认值,如果 source 的字段为 null 生效

通用

对于一些非常常用的方法,我们可以把方法放到通用的类中,使用的时候用 @Mapper 注解的 uses 属性引进来。我们以字符串数组转换为字符串 为例。

字符串数组转换为字符串

List<String>String 的转换,我们默认以 , 作分隔,将字符串拼接起下,Mapper 接口如下

1
2
3
4
5
6
@Mapper
interface ListStrMapper {
default String toStr(List<String> strings) {
return String.join(",", strings);
}
}

使用的时候,直接将 Mapper 类引进来就可以 ListStrMapper 类中的 toStr 方法就像写在 DomainMapper 中一样

1
2
3
@Mapper(uses = {ListStrMapper.class})
interface DomainMapper {
}

但是显然我们如果不只是需要 , 号分隔的字符串应该如何实现?比如默认用逗号,可以用额外的参数来配置。

传参配置 Mapper 方法

MapStruct 支持通过 @Context 注解,将参数传递给方法中,我们将上面的 ListStrMapper 调整一下,增加个参数 delimiter 让用户可以自定义分隔符。

1
2
3
4
5
6
@Mapper
interface ListStrMapper {
default String toStr(List<String> strings, @Context String delimiter) {
return String.join(delimiter, strings);
}
}

这样使用的时候也需要传递 @Context 参数

1
2
3
4
5
6
@Mapper(uses = ListStrMapper.class)
interface DomainMapper {
Domain toDomain(Dto dto, @Context String delimiter);


}

为了方便,我们可以增加一个重载方法,使用默认的分隔符

1
2
3
4
5
6
7
8
@Mapper(uses = ListStrMapper.class)
interface DomainMapper {
Domain toDomain(Dto dto, @Context String delimiter);

default Domain toDomain(Dto dto) {
return toDomain(dto, ",");
}
}

如果想直接在 ListStrMapper 类中实现默认方法的话需要使用 qualified 的方式,因为 MapStruct 目前是无法将有没有 @Context 分开的,所以需要在其它方法上将加上 @Namedqualified 注解, 而默认的方法上不加该注解。

这样设置,如果没在 @Mapping 注解上使用 qualified 属性,MapStruct 将使用没注解的默认方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper
interface ListStrMapper {
String TO_STR_WITH_DELIMITER = "toStrWithDelimiter";

@Named(TO_STR_WITH_DELIMITER)
default String toStrWithDelimiter(List<String> strings, @Context String split) {
return String.join(split, strings);
}

default String toStr(List<String> strings) {
return toStr(strings, ",");
}
}

使用的 Mapper 类也要做相应的调整,如果使用下面这样的会直接使用 toStr 方法,不管是否提供了 @Context

1
2
@Mapping(source = "stringList", target = "string")
Domain toDomain(Dto dto, @Context String delimiter);

而指定特定名字就没问题了,以下方法使用的是 toStrWithDelimiter

1
2
3
4
5
@Mapping(source = "stringList", target = "string", qualifiedByName = {ListStrMapper.TO_STR_WITH_DELIMITER})
Domain toDomain(Dto dto, @Context String delimiter);

@InheritConfiguration
Domain toDomain(Dto dto);

没有 @Context 的直接用 @InheritConfiguration 来把上一个方法的配置继承下来。

总结

MapStruct 是一个高效易用的工具,完全可以替换各种基于反射的 BeanUtils 工具。