MapStruct实用

Maven 依赖

pom.xml 中加入以下依赖

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
<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>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

如果使用了 lombok 还需要再添加以下内容

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>
<!-- lombok 需要添加以下内容 -->
<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>

MapStruct 是用来做两个对象之间的映射关系,在平常的开发中,我们经常会使用到 BeanUtils 这种工具。BeanUtils 的问题在于,他对每个成员亦是赋值是通过反射来做的,并且成员变量的字段名必须一致。而 MapStruct 是在编译期完成这件事情的,还可以将不同名称,不同类型的成员变量进行赋值。

定义 Mapper

在接口上打上 @Mapper 注解, MapStruct 会生成对应的接口实现。

1
2
3
4
@Mapper
public interface Convert {

}

使用 Convert

普通方式(推荐)

1
2
3
4
5
6
@Mapper
public interface Convert {
Convert INSTANCE = Mappers.getMapper(Convert.class);
@Mapping(source = "createTime", target = "createTime", dateFormat = "yyyy-MM-dd")
Person personDTO2Person(PersonDTO personDTO);
}

调用映射使用时,Convert.INSTANCE.personDTO2Person(personDTO) 即可。

Spring方式

1
2
3
4
5
@Mapper(componentModel = "spring")
public interface PersonConvert {
@Mapping(source = "createTime", target = "createTime", dateFormat = "yyyy-MM-dd")
Person personDTO2Person(PersonDTO personDTO);
}
  • 会检查 classpath 下是否饮食 spring 相关依赖

使用时, 用 Spring 的注解 @Autowired 注入。

@Mapping 注解

source

  • 指定源名称,即要映射的对象的名称
  • 不能与constant 或 expression

target

  • 指定目标名称,即被映射的对象名称

constant

  • 将目标对象赋值为指定的常量
  • 不能与source , defaultValue , defaultExpression 或 expression 同时存在

dateFormat

  • 将从 String 映射到 DateSimpleDateFormat 进行处理。

numberFormat

  • 将从 Number 映射到 String , DecimalFormat 进行处理。其他类型将被忽略

expression

  • 使用 java 表达式进行映射。 格式为: java()。例:

    1
    2
    @Mapping(target = "name", expresession = "java(new Date().getTime())")
    Person personDTO2Person(PersonDTO personDTO);
  • 不能与 与source, defaultValue , defaultExpression 和 constant 同时使用

defaultExpression

  • 作用与 expression 类似,在 sourcenull 的时候应用。

  • 不能与 expression , defaultValue 或 constant

ignore

  • 指定字段忽略

defaultValue

  • sourcenull 的时候,为 target 字段指定默认值。

qualifiedBy

qualifiedByName

resultType

dependsOn

nullValueCheckStrategy

nullValuePropertyMappingStrategy

映射技巧

相同类型相同名称的映射

  • 直接写对应的映射方法即可,在参数和返回值分别写对象的 Bean 对象即可。
1
2
3
4
5
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
Person personDTO2Person(PersonDTO personDTO);
}

相同类型不同名称的映射

  • 使用 @Mapping 注解标注对应的 sourcetargetsource 源对象的名称, target 目标名称。
1
2
3
4
5
6
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "createTime", target = "time")
Person personDTO2Person(PersonDTO personDTO);
}

不同类型相同名称的映射

int 到 String

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
55
@Data
public class Person {
private String name;
private String age;
private LocalDate createTime;
}

@Data
public class PersonDTO {
private String name;
private Integer age;
private LocalDate createTime;
}

@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
Person personDTO2Person(PersonDTO personDTO);
}

// PersonDTO 映射到 Person,age 字段在 Person 中是 String,PersonDTO 中是 Integer,直接映射即可,不用加额外的东西
// 生成的代码如下:
public class PersonConvertImpl implements PersonConvert {
@Override
public Person personDTO2Person(PersonDTO personDTO) {
if ( personDTO == null ) {
return null;
}
Person person = new Person();
person.setName( personDTO.getName() );
if ( personDTO.getAge() != null ) {
// 这里会自动加上转换的代码。
person.setAge( String.valueOf( personDTO.getAge() ) );
}
person.setCreateTime( personDTO.getCreateTime() );
return person;
}
}
// 反过来映射, personDTO 中是 String, Person 中是 Integer,生成的代码如下:
public class PersonConvertImpl implements PersonConvert {
@Override
public Person personDTO2Person(PersonDTO personDTO) {
if ( personDTO == null ) {
return null;
}
Person person = new Person();
person.setName( personDTO.getName() );
if ( personDTO.getAge() != null ) {
// 这里会自动加上转换的代码。
person.setAge( Integer.parseInt( personDTO.getAge() ) );
}
person.setCreateTime( personDTO.getCreateTime() );
return person;
}
}

enum 到 String

  • 调用对应枚举的 name() 方法,然后再 set 到对应字段。
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
@Data
public class Person {
private String name;
private Integer age;
private LocalDate createTime;
private String sex;
}

@Data
public class Person {
private String name;
private Integer age;
private LocalDate createTime;
private String sex;
}

// 转换 Convert
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
Person personDTO2Person(PersonDTO personDTO);
}

// 生成的代码
public class PersonConvertImpl implements PersonConvert {
@Override
public Person personDTO2Person(PersonDTO personDTO) {
if ( personDTO == null ) {
return null;
}
Person person = new Person();
person.setName( personDTO.getName() );
if ( personDTO.getAge() != null ) {
person.setAge( Integer.parseInt( personDTO.getAge() ) );
}
person.setCreateTime( personDTO.getCreateTime() );
if ( personDTO.getSex() != null ) {
person.setSex( personDTO.getSex().name() );
}
return person;
}
}
  • 思考:如果是枚举中的某个字段,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Getter
    public enum SexEnum {
    MAN(1),
    WOMAN(0);
    private int sex;
    SexEnum(int sex) {
    this.sex = sex;
    }
    }

    其中的数字要映射到对应的对象如何转换? Person 定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Data
    public class Person {
    private String name;
    private Integer age;
    private LocalDate createTime;
    // 1-男,0-女
    private Integer sex;

    }
  • 反过来呢? 0/1 映射成对应的枚举。

BigDecimal 到 String

  • 将数字进行格式化处理
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
55
@Data
public class PersonDTO {
private String name;
private String age;
private LocalDate createTime;
private SexEnum sex;
private BigDecimal power;
}

@Data
public class Person {
private String name;
private Integer age;
private LocalDate createTime;
private String sex;
private String power;
}

// 映射 Convert
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(target = "power", source = "power", numberFormat = "#.##E0")
Person personDTO2Person(PersonDTO personDTO);
}

// 生成的代码,使用 DecimalFormat 进行格式化处理
public class PersonConvertImpl implements PersonConvert {
@Override
public Person personDTO2Person(PersonDTO personDTO) {
if ( personDTO == null ) {
return null;
}
Person person = new Person();
if ( personDTO.getPower() != null ) {
person.setPower( createDecimalFormat( "#.##E0" ).format( personDTO.getPower() ) );
}
person.setName( personDTO.getName() );
if ( personDTO.getAge() != null ) {
person.setAge( Integer.parseInt( personDTO.getAge() ) );
}
person.setCreateTime( personDTO.getCreateTime() );
if ( personDTO.getSex() != null ) {
person.setSex( personDTO.getSex().name() );
}
return person;
}

private DecimalFormat createDecimalFormat( String numberFormat ) {

DecimalFormat df = new DecimalFormat( numberFormat );
df.setParseBigDecimal( true );
return df;
}
}

Date 到 String

  • 支持 Date 、Calendar、LocalDate、Instant、ZonedDateTime、LocalDateTime 等多种类型
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
@Data
public class PersonDTO {
private String name;
private String age;
private LocalDate createTime;
private SexEnum sex;
}

@Data
public class Person {
private String name;
private Integer age;
private String createTime;
private String sex;
}

// 映射的 Convert,
@Mapper
public interface PersonConvert {

PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);

@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
Person personDTO2Person(PersonDTO personDTO);
}

// 生成的代码, 使用 DateTimeFormatter 进行格式,如果是 Date 会使用 SimpleDateFormt 进行格式化。
public class PersonConvertImpl implements PersonConvert {
@Override
public Person personDTO2Person(PersonDTO personDTO) {
if ( personDTO == null ) {
return null;
}

Person person = new Person();
if ( personDTO.getCreateTime() != null ) {
person.setCreateTime( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ).format( personDTO.getCreateTime() ) );
}
person.setName( personDTO.getName() );
if ( personDTO.getAge() != null ) {
person.setAge( Integer.parseInt( personDTO.getAge() ) );
}
if ( personDTO.getSex() != null ) {
person.setSex( personDTO.getSex().name() );
}
return person;
}
}

不同类型不同名称的映射

  • 需要在 @Mapping 注解的 target 中指定目标名称。
1
2
3
4
5
6
7
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);

@Mapping(source = "createTime", target = "time", dateFormat = "yyyy-MM-dd HH:mm:ss")
Person personDTO2Person(PersonDTO personDTO);
}

多个同名参数的成员变量

  • 在转换的方法中有多个参数,参数为 DTO 对象,多个对象可能会出现重名的情况。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Data
    public class PersonDTO {
    private String name;
    }

    @Data
    public class UserDTO {
    private String name;
    }

    // 映射后的对象, userDTO.name -> nickname; personDTO.name -> name;
    @Data
    public class Person {
    private String name;
    private String nickName;
    }
  • 映射时需要指定映射的名称

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
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);

@Mapping(source = "userDTO.name", target = "nickName")
@Mapping(source = "personDTO.name", target = "name")
Person personDTOUSer2Person(PersonDTO personDTO, UserDTO userDTO);
}

// 生成的代码
public class PersonConvertImpl implements PersonConvert {
@Override
public Person personDTOUSer2Person(PersonDTO personDTO, UserDTO userDTO) {
if ( personDTO == null && userDTO == null ) {
return null;
}

Person person = new Person();
if ( personDTO != null ) {
person.setName( personDTO.getName() );
}
if ( userDTO != null ) {
person.setNickName( userDTO.getName() );
}
return person;
}
}

在参数上映射而不返回值

  • 上面的映射全都是把参数映射成对应的返回值,我们接收到的返回值是映射后的对象。这里的问题是,返回对象是新 new 的,如果我们已经存在一个对象,映射部分字段想要传入一个已经手动 new 好的对象来映射。
  • 使用 @MappingTarget 来标记返回的对象。
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
@Data
public class PersonDTO {
private String name;
}

@Data
public class UserDTO {
private String name;
}

@Data
public class Person {
private String name;
private String nickName;
}

@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);

@Mapping(source = "userDTO.name", target = "nickName")
@Mapping(source = "personDTO.name", target = "name")
void personDTOUSer2Person(PersonDTO personDTO, UserDTO userDTO, @MappingTarget Person person);
}

// 生成的代码
public class PersonConvertImpl implements PersonConvert {
@Override
public void personDTOUSer2Person(PersonDTO personDTO, UserDTO userDTO, Person person) {
if ( personDTO == null && userDTO == null ) {
return;
}
if ( personDTO != null ) {
person.setName( personDTO.getName() );
}
if ( userDTO != null ) {
person.setNickName( userDTO.getName() );
}
}
}

成员变量直接映射

  • 即没有 get 和 set 方法,而是直接使用 public 定义的成员变量
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
public class PersonDTO {
public String name;
public Integer age;
}

public class Person {
public String name;
public Integer age;
}

// 映射
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
Person personDTOUSer2Person(PersonDTO personDTO);
}

// 生成的代码
public class PersonConvertImpl implements PersonConvert {
@Override
public Person personDTOUSer2Person(PersonDTO personDTO) {
if ( personDTO == null ) {
return null;
}
Person person = new Person();
person.name = personDTO.name;
person.age = personDTO.age;
return person;
}
}

嵌套的 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
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
55
56
57
58
59
60
61
@Data
public class PersonDTO {
private String name;
private Integer age;
private Record record;

@Data
public static class Record {
private String recordName;
private Integer number;
}
}

@Data
public class Person {
private String personName;
private Integer age;
private Integer account;
}
// 将 PersonDTO 中的 name 映射为 personName
// age 映射为 age
// Record 中的 number 映射为 Person 中的 account

@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);

@Mapping(source = "record.number", target = "account")
@Mapping(source = "name", target = "personName")
Person personDTOUSer2Person(PersonDTO personDTO);
}

// 生成的代码
public class PersonConvertImpl implements PersonConvert {
@Override
public Person personDTOUSer2Person(PersonDTO personDTO) {
if ( personDTO == null ) {
return null;
}
Person person = new Person();
person.setAccount( personDTORecordNumber( personDTO ) );
person.setPersonName( personDTO.getName() );
person.setAge( personDTO.getAge() );
return person;
}

private Integer personDTORecordNumber(PersonDTO personDTO) {
if ( personDTO == null ) {
return null;
}
Record record = personDTO.getRecord();
if ( record == null ) {
return null;
}
Integer number = record.getNumber();
if ( number == null ) {
return null;
}
return number;
}
}

Builder 模式映射

  • 被映射代码是基于 Builder 模式设计的
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
@Data
public class PersonDTO {
private String name;
private Integer age;
}

@Builder
public class Person {
private String name;
private Integer age;
}

// 映射器
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
Person personDTOUSer2Person(PersonDTO personDTO);
}
// 生成的映射器代码
public class PersonConvertImpl implements PersonConvert {
@Override
public Person personDTOUSer2Person(PersonDTO personDTO) {
if ( personDTO == null ) {
return null;
}
PersonBuilder person = Person.builder();
person.name( personDTO.getName() );
person.age( personDTO.getAge() );
return person.build();
}
}

构造器映射

  • 只有一个公共的构造器
  • 有一个 @Default 注解,可以来自任何包。
  • 有一个无参构造器,同时还包含其他构造器。
  • 有多个公共构造器,不含无参构造器,(编译错误)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
@AllArgsConstructor
public class PersonDTO {

private String name;

private Integer age;
}

@Getter
@AllArgsConstructor
public class Person {

private String name;

private Integer age;
}

只有一个公共的构造器

1

集合映射

Map映射

其他用法

向映射器添加自定义方法

  • 在接口中写一个 default 方法即可,这其实就是自己手动写个方法。**(很少会用)**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Mapper
public interface PersonConvert {
PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
@Mapping(source = "createTime", target = "createTime", dateFormat = "yyyy-MM-dd")
Person personDTO2Person(PersonDTO personDTO);

default PersonDTO person2PersonDTO(Person person) {
PersonDTO dto = new PersonDTO();
dto.setName("张三");
dto.setAge(person.getAge() + 13);
dto.setCreateTime("这个时间很奇怪,我必须手动实现");
return dto;
}
}
  • 在抽象类中实现。也是一样写一个 public 的方法即可,Mapstruct 的实现是,继承我们自己写的那个抽象类。(基本不用)