目录

知识 - MapStruct

# 介绍

对于代码中 JavaBean之间的转换, 一直是困扰我很久的事情。在开发的时候我看到业务代码之间有很多的 JavaBean 之间的相互转化, 非常的影响观感,却又不得不存在。我后来想的一个办法就是通过反射,或者自己写很多的转换器。

第一种通过反射的方法确实比较方便,但是现在无论是 BeanUtils, BeanCopier 等在使用反射的时候都会影响到性能。虽然我们可以进行反射信息的缓存来提高性能。但是像这种的话,需要类型和名称都一样才会进行映射,有很多时候,由于不同的团队之间使用的名词不一样,还是需要很多的手动 set/get 等功能。

第二种的话就是会很浪费时间,而且在添加新的字段的时候也要进行方法的修改。不过,由于不需要进行反射,其性能是很高的。

针对第二种,我们就可以使用 MapStruct。

MapSturct 是一个生成类型安全,高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。

# 依赖

这里使用的是最新版:(2023-07-12)

<properties>
  <maven.compiler.source>8</maven.compiler.source>
  <maven.compiler.target>8</maven.compiler.target>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${mapstruct.version}</version>
  </dependency>
  <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${mapstruct.version}</version>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
    <optional>true</optional>
  </dependency>
</dependencies>
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

用到了 Lombok 和 MapStruct 进行搭配。

但是目前这两个同时作为最新版,会有冲突问题,即 Maven 默认使用了 MapStruct 的处理器,导致 Lombok 的所有注解都失效,如 @Setter@Getter 注解在和 MapStruct 搭配时,不会生产 Setter、Getter 方法,导致报错。

所以需要告诉 Maven,先使用 Lombok,再使用 MapStruct。

<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>${mapstruct.version}</version>
          </path>
          <path>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
          </path>
          <!-- This is needed when using Lombok 1.18.16 and above -->
          <path>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <version>0.2.0</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>
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

如果不使用 Lombok,则需要手动对属性生成 Setter、Getter 等方法。

# 映射方式

# 隐式映射

这里演示从 PersonDTO 转换为 PersonVO。

PersonDTO 类:

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class PersonDTO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;   
}
1
2
3
4
5
6
7
8
9
10
11
12

PersonVO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

可以看到,两个类除了名字不一样,属性的类型和名字都一样,所以我们用 MapStruct 进行转换的时候是最方便的。

定义一个 PersonConvert 类,用于将 DTO 转为 VO。一般将一个 Package 为 convert,专门存放 MapStruct 转换的类。

@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
  
    PersonVO personVOToDTO(PersonDTO personDTO);
}
1
2
3
4
5
6

@Mapper 是 MapStruct 自带的类,类似于 SpringBoot 的 @Component 等注解,标识该类是 MapStruct 的处理类。

public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setId(127);
        personDTO.setName("可乐");
        personDTO.setAge(24);
        personDTO.setAddress("深圳");
        personDTO.setBirthDate(new Date());
        personDTO.setIsMarried(true);
        PersonVO personVO = PersonConvert.INSTANCE.personVOToDTO(personDTO);
        System.out.println(personVO);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

打印的结果:

PersonVO(id=127, name=可乐, age=24, address=深圳, birthDate=Tue Jul 11 23:58:06 CST 2023, isMarried=true)
1

如果我们去看编译后的 class 类可以发现,MapStruct 自动生成了一个 PersonConvertImpl.class,里面就是 set 和 get 方法,如下:

public class PersonConvertImpl implements PersonConvert {
    public PersonConvertImpl() {
    }

    public PersonVO personVOToDTO(PersonDTO personDTO) {
        if (personDTO == null) {
            return null;
        } else {
            PersonVO personVO = new PersonVO();
            personVO.setId(personDTO.getId());
            personVO.setName(personDTO.getName());
            personVO.setAge(personDTO.getAge());
            personVO.setAddress(personDTO.getAddress());
            personVO.setBirthDate(personDTO.getBirthDate());
            personVO.setIsMarried(personDTO.getIsMarried());
            return personVO;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这个自动生成的类非常熟悉,就是我们常用的 set 和 get 方法。

# 显式映射

上面的自动映射是当两个类的属性名一样的情况下,MapStruct 自动映射和注入,但是当属性名不一样的时候,我们就需要告诉 MapStruct,Source 类的哪个属性名对应 Target 的哪个属性名。

这个前提是类型保持一致,或者是 int 和 Integer 的能自动转化的关系。

PersonDTO 类:

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

PersonVO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private Integer id;
    private String personName;
    private Integer personAge;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

可以看到,DTO 的 name 对应 VO 的 personName,DTO 的 age 对应 VO 的 personAge,但是 MapStcut 并不知道,所以我们需要告诉它。

如下:

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

    @Mappings(
            value = {
                    @Mapping(source = "name", target = "personName"),
                    @Mapping(source = "age", target = "personAge")
            }
    )
    PersonVO personVOToDTO(PersonDTO personDTO);

}
1
2
3
4
5
6
7
8
9
10
11
12
13

用一个 @Mappings 包裹多个 @Mapping,每个 @Mapping 使用了两个属性:source 和 target。

其中 source 是指定 PersonDTO(参数)的属性名,target 是指定 PersonVO(返回值)的属性名,这样就形成了对应关系,MapStruct 将 source 属性的值注入到 target 属性里。

public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setId(127);
        personDTO.setName("可乐");
        personDTO.setAge(24);
        personDTO.setAddress("深圳");
        personDTO.setBirthDate(new Date());
        personDTO.setIsMarried(true);
        PersonVO personVO = PersonConvert.INSTANCE.personVOToDTO(personDTO);
        System.out.println(personVO);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

打印结果:

PersonVO(id=127, personName=可乐, personAge=24, address=深圳, birthDate=Wed Jul 12 00:03:00 CST 2023, isMarried=true)
1

当然,上面 PersonConvert 用 @Mappings 包裹多个 @Mapping 比较麻烦,我们可以直接使用精简版:

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

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO personVOToDTO(PersonDTO personDTO);

}
1
2
3
4
5
6
7
8
9

直接使用多个 @Mapping 即可。

# 集合映射

当属性是一个集合时,无论使用隐式还是显式映射,MapStruct 都会根据是否是集合来进行自动映射,如:

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private List<Integer> id;
}

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private List<Integer> id;
}

@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
  
    PersonVO personVOToDTO(PersonDTO personDTO);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

MapStruct 会自动处理集合的映射,底层就是 for 循环处理 Integer 映射。

# 忽略映射

# 指定属性忽略映射

MapStruct 默认是把相同属性名的属性进行映射,但是我们可能不需要某些字段进行映射,那么我们可以用 @Mapping 的 ignore 来告诉 MapStruct 进行忽视,如:

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
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

我们希望不将 PersonDTO 的 name 和 address 转换到 PersonVO 里,则

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

    @Mapping(target = "name", ignore = true)
    @Mapping(target = "address", ignore = true)
    PersonVO convert(PersonDTO personDTO);

}
1
2
3
4
5
6
7
8
9

运行类:

public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setId(127);
        personDTO.setName("可乐");
        personDTO.setAge(24);
        personDTO.setAddress("深圳");
        personDTO.setBirthDate(new Date());
        personDTO.setIsMarried(true);
        PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
        System.out.println("转换成功的 personVO:" + personVO);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

打印:

转换成功的 personVO:PersonVO(id=127, name=null, age=24, address=null, birthDate=Sat Jul 15 00:37:10 CST 2023, isMarried=true)
1

可以看到 name 和 address 都被忽视了。

# 全相等属性忽略映射

有时候我们希望只有 Source 类和 Target 类的属性名一样,则进行忽略,但是如:

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
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

如果我们使用上面的 ignore 进行忽略,则需要写 6 个 @Mapping(target = "xxx", ignore = true),这明显不优雅,那么我们可以使用 MapStruct 提供的另一个注解 @BeanMapp 的 ignoreByDefault 属性进行全相等属性忽略映射。

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

    @BeanMapping(ignoreByDefault = true)
    PersonVO convert(PersonDTO personDTO);

}
1
2
3
4
5
6
7
8

运行类:

public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setId(127);
        personDTO.setName("可乐");
        personDTO.setAge(24);
        personDTO.setAddress("深圳");
        personDTO.setBirthDate(new Date());
        personDTO.setIsMarried(true);
        PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
        System.out.println("转换成功的 personVO:" + personVO);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

打印:

转换成功的 personVO:PersonVO(id=null, name=null, age=null, address=null, birthDate=null, isMarried=null)
1

可以看到所有相同名字的属性都被忽视了。

# 指定属性映射

如果我们只想一两个属性进行映射,其他都忽略,则在 全相等属性忽略映射 基础上,使用 @Mapping 即可。

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
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);

    @BeanMapping(ignoreByDefault = true)
    @Mapping(source = "name", target = "name")
    @Mapping(source = "address", target = "address")
    PersonVO convert(PersonDTO personDTO);

}
1
2
3
4
5
6
7
8
9
10

运行类:

public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setId(127);
        personDTO.setName("可乐");
        personDTO.setAge(24);
        personDTO.setAddress("深圳");
        personDTO.setBirthDate(new Date());
        personDTO.setIsMarried(true);
        PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
        System.out.println("转换成功的 personVO:" + personVO);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

打印:

转换成功的 personVO:PersonVO(id=null, name=可乐, age=null, address=深圳, birthDate=null, isMarried=null)
1

可以看到 name、address 属性进行映射,其他属性被忽略映射。

# 获取 Mapper 实例

我们如何获取经过 @Mapper 修饰的类呢?上面的例子其实以及说明了,通过 Mappers.getMapper(xxx.class) 的方式来进行对应 Mapper 的获取。此种方法为通过 Mapper 工厂获取。

如果是此种方法,约定俗成的是在接口内定义一个接口本身的实例 INSTANCE,以方便获取对应的实例。

@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
  
    PersonVO personVOToDTO(PersonDTO personDTO);
}
1
2
3
4
5
6

当然我们也可以在外面的类通过 Mappers.getMapper(PersonConvert.class) 获取 PersonConvert 实例,只是将实例放在里面,是约定俗成。

# 映射前置 & 后置

和拦截器类似,MapStruct 在执行映射前后,都有一次前置和后置的回调,所以我们可以在执行映射前或者后,对映射的两个类进行特殊处理。

@BeforeMapping 注解作用于方法上,注解标注的方法会在映射方法中首先执行。

@AfterMapping 注解同样作用于方法之上,标记要在生成的映射方法的末尾调用的方法,就 在映射方法的最后一个返回语句之前

可以同时用 @BeforeMapping 注解标注多个方法,也可以同时用 @AfterMapping 注解标注多个方法。在这种情况下,在生成的实现类中,这些方法会按照我们在接口或者抽象类中定义的顺序执行。

该方法(两个注解指定的方法)既可以在抽象映射器类中实现,也可以通过 @Mapper 注解的 uses 属性指定的类型(类或接口)声明,还可以在用 @Context 参数的类型中实现,以便在映射方法中使用。

PersonDTO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class PersonDTO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
1
2
3
4
5
6
7
8
9
10
11
12

PersonVO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private Integer id;
    private String personName;
    private Integer personAge;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

PersonConvert 类

@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
    
    @BeforeMapping
    default void beforeMapping(PersonDTO personDTO) {
        System.out.println("beforeMapping:" + personDTO);
    }

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO personVOToDTO(PersonDTO personDTO);

    @AfterMapping
    default void afterMapping(PersonDTO personDTO, @MappingTarget PersonVO personVO) {
        System.out.println("afterMapping - personDTO:" + personDTO);
        System.out.println("afterMapping - personVO:" + personVO);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

@BeforeMapping 修饰的方法只有 Source 类,因为还没有进行映射,所以 Target 类是 null。

@AfterMapping 参数为了区分哪个是 Source 类和 Target 类,则需要用 @MappingTarget 标明 Target 类。

public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setId(127);
        personDTO.setName("可乐");
        personDTO.setAge(24);
        personDTO.setAddress("深圳");
        personDTO.setBirthDate(new Date());
        personDTO.setIsMarried(true);
        PersonVO personVO = PersonConvert.INSTANCE.personVOToDTO(personDTO);
        System.out.println("转换成功的 personVO:" + personVO);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

打印:

beforeMapping:PersonDTO(id=127, name=可乐, age=24, address=深圳, birthDate=Wed Jul 12 00:34:38 CST 2023, isMarried=true)
afterMapping - personDTO:PersonDTO(id=127, name=可乐, age=24, address=深圳, birthDate=Wed Jul 12 00:34:38 CST 2023, isMarried=true)
afterMapping - personVO:PersonVO(id=127, personName=可乐, personAge=24, address=深圳, birthDate=Wed Jul 12 00:34:38 CST 2023, isMarried=true)
转换成功的 personVO:PersonVO(id=127, personName=可乐, personAge=24, address=深圳, birthDate=Wed Jul 12 00:34:38 CST 2023, isMarried=true)
1
2
3
4

@AfterMapping 的使用场景比 @BeforeMapping 更多一下,如我们有些属性是经过其他属性计算而得出的,所以可以统一在 @AfterMapping 进行计算,因为此时其他属性都已经被注入了。

比如当年龄大于 24 后。则默认已经结婚:

@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
   
    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO personVOToDTO(PersonDTO personDTO);

    @AfterMapping
    default void afterMapping(PersonDTO personDTO, @MappingTarget PersonVO personVO) {
        // 当年龄大于 24 后。则默认已经结婚
        if(personVO.getPersonAge() == 24) {
            personVO.setIsMarried(true);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# uses 使用

如果我们有很多 MapStruct 的转换类,其中有部分的逻辑是重复的,那么我们可以将重复的逻辑封装到一个公共的类,然后通过 uses 来引用这个公共的类。

如上面使用了 @BeforeMapping@AfterMapping,我们将这两个方法放到一个类

public class BeforeAfterMapping {

    @BeforeMapping
    public void beforeMapping(PersonDTO personDTO) {
        System.out.println("beforeMapping:" + personDTO);
    }

    @AfterMapping
    public void afterMapping(PersonDTO personDTO, @MappingTarget PersonVO personVO) {
        System.out.println("afterMapping - personDTO:" + personDTO);
        System.out.println("afterMapping - personVO:" + personVO);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

然后 uses 引用

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

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO personVOToDTO(PersonDTO personDTO);

}
1
2
3
4
5
6
7
8
9

uses 支持多个类,逗号隔开。

uses 功能非常强大,我们将重复的逻辑在外面封装,然后使用 uses 引入进来,这对代码的阅读性和维护性都有很大的帮助。

# 引用类属性映射

# Mapping 映射

当一个实体类引用了另一个实体类,MapStruct 也能进行映射。

PersonDTO 类和引用的 SubPersonDTO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
    private SubPersonDTO subPersonDTO;
}

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class SubPersonDTO {
    private Integer id;
    private String dtoName;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

PersonVO 类和引用的 SubPersonVO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private Integer id;
    private String personName;
    private Integer personAge;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
    private SubPersonVO subPersonVO;
}

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class SubPersonVO {
    private Integer id;
    private String voName;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

然后将 PersonDTO 的属性值注入到 PersonVO 里,包括引用的类。

@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
    
    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    @Mapping(source = "subPersonDTO", target = "subPersonVO")
    PersonVO personVOToDTO(PersonDTO personDTO);

    @Mapping(source = "dtoName", target = "voName")
    SubPersonVO subPersonVOToDTO(SubPersonDTO subPersonDTO);
}

1
2
3
4
5
6
7
8
9
10
11
12
13

MapStruct 如果检测到是引用类型,则会在当前接口/类找引用类型的 Mapping 方法,如果有则进行对应的转换,没有则为 null。

public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setId(127);
        personDTO.setName("可乐");
        personDTO.setAge(24);
        personDTO.setAddress("深圳");
        personDTO.setBirthDate(new Date());
        personDTO.setIsMarried(true);
        personDTO.setSubPersonDTO(createSubPersonDTO());
        
        PersonVO personVO = PersonConvert.INSTANCE.personVOToDTO(personDTO);
        
        System.out.println("转换成功的 personVO:" + personVO.getSubPersonVO());
    }
    
    public static SubPersonDTO createSubPersonDTO() {
        SubPersonDTO subPersonDTO = new SubPersonDTO();
        subPersonDTO.setId(10);
        subPersonDTO.setDtoName("冰糖");
        return subPersonDTO;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

打印:

转换成功的 personVO:SubPersonVO(id=10, voName=冰糖)
1

# 自定义映射规则

基于上面例子,SubPersonDTO 映射到 SubPersonVO,可以直接使用 @Mapping 进行 dtoName 到 voName 的映射,当涉及到复杂的逻辑,我们可以自定义映射。

还是使用上面的例子,我们只需要写具体的方法实现逻辑即可:

@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
    
    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    @Mapping(source = "subPersonDTO", target = "subPersonVO")
    PersonVO personVOToDTO(PersonDTO personDTO);

    default SubPersonVO subPersonVOToDTO(SubPersonDTO subPersonDTO) {
        SubPersonVO subPersonVO = new SubPersonVO();
        subPersonVO.setId(subPersonDTO.getId());
        subPersonVO.setVoName("雪梨");
        return subPersonVO;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

和下面对比

@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
    
    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    @Mapping(source = "subPersonDTO", target = "subPersonVO")
    PersonVO personVOToDTO(PersonDTO personDTO);

    @Mapping(source = "dtoName", target = "voName")
    SubPersonVO subPersonVOToDTO(SubPersonDTO subPersonDTO);
}
1
2
3
4
5
6
7
8
9
10
11
12

我们实现了自定义映射,不使用 MapStruct 的规则,我们指定 subPersonVO 的 voName 为雪梨。

所以打印:

转换成功的 personVO:SubPersonVO(id=10, voName=雪梨)
1

# 多级嵌套映射

当类 A 引用了类 B,类 B 引用了类 C,那么在映射的过程,依然可以用上面两个规则来映射,只不过多加了一个类 B 到类 C 的映射方法。

# 多转一

我们在实际的业务中少不了将多个对象转换成一个的场景。MapStruct 当然也支持多转一的操作。

如我们将 PersonDTO 和 PersonVO 的属性映射到 Person。

PersonDTO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

PersonVO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private Integer id;
    private String name;
    private Integer sge;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Person 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Person {
    private Integer id;
    private String personDTOName;
    private Integer personDTOAge;
    private String personVOName;
    private Integer personVOAge;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

personDTOName 和 personDTOAge 是 PersonDTO 的属性映射,personVOName 和 personVOAge 是 PersonVO 的属性映射。

但是 id、address、birthDate、isMarried 是 DTO 还是 VO 的属性映射呢?这需要我们手动去指定使用哪个类的属性。

@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
    
    @Mapping(source = "personDTO.name", target = "personDTOName")
    @Mapping(source = "personDTO.age", target = "personDTOAge")
    @Mapping(source = "personVO.name", target = "personVOName")
    @Mapping(source = "personVO.age", target = "personVOAge")
    @Mapping(source = "personDTO.id", target = "id")
    @Mapping(source = "personDTO.address", target = "address")
    @Mapping(source = "personVO.birthDate", target = "birthDate")
    @Mapping(source = "personVO.isMarried", target = "isMarried")
    Person personVOToDTO(PersonVO personVO, PersonDTO personDTO);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

source 的 xxx.xx,其中 xxx 就是对象名,xx 就是属性名。

public class Main {
    public static void main(String[] args) {
        Person person = PersonConvert.INSTANCE.personVOToDTO(createPersonVO(), createPersonDTO());
        System.out.println("转换成功的 person:" + person);
    }

    public static PersonVO createPersonVO() {
        PersonVO personVO = new PersonVO();
        personVO.setId(199);
        personVO.setName("冰糖");
        personVO.setAge(20);
        personVO.setAddress("深圳");
        personVO.setBirthDate(new Date());
        personVO.setIsMarried(false);
        return personVO;
    }
    
    public static PersonDTO createPersonDTO() {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setId(127);
        personDTO.setName("可乐");
        personDTO.setAge(24);
        personDTO.setAddress("广西");
        personDTO.setBirthDate(new Date());
        personDTO.setIsMarried(true);
        return personDTO;
    }
    
}
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

打印:

转换成功的 person:Person(id=127, personDTOName=可乐, personDTOAge=24, personVOName=冰糖, personVOAge=20, address=广西, birthDate=Thu Jul 13 00:04:37 CST 2023, isMarried=false)
1

当然如果配合 @AfterMapping,我们可以这样获取三个参数:

@AfterMapping
default void afterMapping(PersonVO personVO, PersonDTO personDTO, @MappingTarget Person person) {

}
1
2
3
4

# 更新 Bean 对象

有时候,我们不是想返回一个新的 Bean 对象,而是希望更新传入对象的一些属性。这个在实际的时候也会经常使用到。

更新 Bean 对象的方式和 @AfterMapping 修饰的方法一样,只不过 更新 Bean 对象需要我们手动调用方法,而 @AfterMapping 修饰的方法是 MapStruct 在映射后自动调用。

PersonDTO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

PersonVO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private Integer id;
    private String personName;
    private Integer personAge;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

将 PersonDTO 的部分属性注入到 PersonVO 里

@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
    
    @Mapping(source = "age", target = "personAge")
    @Mapping(target = "address", ignore = true)
    void updatePersonVO(PersonDTO personDTO, @MappingTarget PersonVO personVO);

}
1
2
3
4
5
6
7
8
9
public class Main {
    public static void main(String[] args) {
        PersonVO personVO = createPersonVO();
        PersonConvert.INSTANCE.updatePersonVO(createPersonDTO(), personVO);
        System.out.println("更新成功的 PersonVO:" + personVO);
    }

    public static PersonVO createPersonVO() {
        PersonVO personVO = new PersonVO();
        personVO.setId(199);
        personVO.setPersonName("冰糖");
        personVO.setAddress("深圳");
        return personVO;
    }
    
    public static PersonDTO createPersonDTO() {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setAge(24);
        personDTO.setBirthDate(new Date());
        personDTO.setIsMarried(true);
        return personDTO;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

打印:

更新成功的 PersonVOPersonVO(id=null, personName=冰糖, personAge=24, address=深圳, birthDate=Thu Jul 13 00:18:29 CST 2023, isMarried=true)
1

MapStruct 会将 PersonDTO 的属性都更新到 PersonVO 里,而这个例子 PersonDTO 的 address 为 null,所以为了继续使用 PersonVO 的 address 深圳,则使用 ignore 参数无视 address 的映射,否则 address 会被映射为 null。

# 继承配置

# 正向继承

当我们一个 MapStruct 类有很多映射规则,并且很多映射规则,我们可以使用 @InheritConfiguration 来继承其他的映射规则,如:

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

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO convert(PersonDTO personDTO);

    @InheritConfiguration
    void update(PersonDTO personDTO, PersonVO personVO);

}
1
2
3
4
5
6
7
8
9
10
11
12

这样 update 就继承了 convert 的配置,即等价于:

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

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO convert(PersonDTO personDTO);

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    void update(PersonDTO personDTO, PersonVO personVO);

}
1
2
3
4
5
6
7
8
9
10
11
12
13

但是当出现多个映射规则,我们就需要使用 @InheritConfiguration 的 name 属性来告诉 MapStruct 继承哪个方法的映射规则,如:

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

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO convert(PersonDTO personDTO);

    @Mapping(source = "name", target = "personName")
    @Mapping(target = "address", ignore = true)
    PersonVO convert2(PersonDTO personDTO);

    @InheritConfiguration(name = "convert2")
    void update(PersonDTO personDTO, PersonVO personVO);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这样 update 就是要 convert2 的映射规则,即等价于:

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

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO convert(PersonDTO personDTO);

    @Mapping(source = "name", target = "personName")
    @Mapping(target = "address", ignore = true)
    PersonVO convert2(PersonDTO personDTO);

    @Mapping(source = "name", target = "personName")
    @Mapping(target = "address", ignore = true)
    void update(PersonDTO personDTO, PersonVO personVO);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 反向继承

我们可能会遇到一种情况就是,我们定义了一个方法将 DTO 转成 VO,这个方法写了很多的 @Mapping 映射规则,但是我们可能又需要将 VO 转为 DTO,此时也需要写很多的 @Mapping,只是该注解的 source 和 target 进行互换,为了避免反向映射导致写重复的很多 @Mapping,我们可以使用 @InheritInverseConfiguration 进行反向继承:

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

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO convert(PersonDTO personDTO);

    @InheritInverseConfiguration
    PersonDTO inverseConvert(PersonVO personVO);

}
1
2
3
4
5
6
7
8
9
10
11
12

此时 inverseConvert 方法就使用了 convert 的规则,然后将规则进行反向编译,即等价于:

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

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO convert(PersonDTO personDTO);

    @Mapping(source = "personName", target = "name")
    @Mapping(source = "personAge", target = "age")
    PersonDTO inverseConvert(PersonVO personVO);

}
1
2
3
4
5
6
7
8
9
10
11
12
13

如果有很多映射规则的方法,则需要利用 @InheritInverseConfiguration 的 name 属性来告诉 MapStruct 反向继承哪个方法的映射规则,如:

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

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO convert(PersonDTO personDTO);
  
  	@Mapping(source = "name", target = "personName")
    PersonVO convert2(PersonDTO personDTO);

    @InheritInverseConfiguration(name = "convert2")
    PersonDTO inverseConvert2(PersonVO personVO);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这样 inverseConvert2 使用的规则就是方法 convert2 的映射规则,即等价于:

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

    @Mapping(source = "name", target = "personName")
    @Mapping(source = "age", target = "personAge")
    PersonVO convert(PersonDTO personDTO);
  
  	@Mapping(source = "name", target = "personName")
    PersonVO convert2(PersonDTO personDTO);

    @Mapping(source = "personName", target = "name")
    PersonDTO inverseConvert2(PersonVO personVO);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 映射 format 规则

# 日期转换

如果我们需要进行日期的转换(Date 转成 String),可以使用 @Mapping 的 dateFormat 属性。

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Date birthDate;
}

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private String birthDate;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

转换类使用 dateFormat 属性转成 yyyy-MM-dd HH:mm:ss

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

    @Mapping(source = "birthDate", target = "birthDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
    PersonVO convert(PersonDTO personDTO);

}
1
2
3
4
5
6
7
8

运行类

public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setBirthDate(new Date());
        PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
        System.out.println("转换成功的 personVO:" + personVO);
    }
}
1
2
3
4
5
6
7
8

运行:

转换成功的 personVO:PersonVO(birthDate=2023-07-15 00:24:35)
1

# 小数点转换

如果我们需要进行 Double 或 Float 转成 String,可以使用 @Mapping 的 numberFormat 属性。

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Double num;
}

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private String num;
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

转换类使用 numberFormat 属性转成保留两位小数点

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

    @Mapping(source = "num", target = "num", numberFormat = "#.00")
    PersonVO convert(PersonDTO personDTO);

}
1
2
3
4
5
6
7
8

运行类

public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setNum(1.2345);
        PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
        System.out.println("转换成功的 personVO:" + personVO);
    }
}
1
2
3
4
5
6
7
8

打印:

转换成功的 personVO:PersonVO(num=1.23)
1

# 依赖注入

# Spring 注入

如果搭配 Spring 使用,MapSturct 也支持使用依赖注入到 Spring 容器,同时也推荐使用依赖注入。

@Mapper(componentModel = "spring")
@Mapper
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
  
    PersonVO personVOToDTO(PersonDTO personDTO);
}
1
2
3
4
5
6
7

添加了 @Mapper(componentModel = "spring"),就等于添加了 @Mapper + @Component

# 依赖注入策略

MapStruct 默认是调用属性的 setter 和 getter 来获取和设置值,那么我们也可以指定通过有参构造器来实现设置值。

@Mapper(componentModel = "cdi", injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
    
    @Mapping(source = "birthDate", target = "birthDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
    PersonVO personVOToDTO(PersonDTO personDTO);

}
1
2
3
4
5
6
7
8

# 自定义类型转换

有时候,在对象转换的时候可能会出现这样一个问题,就是源对象中的类型是 Boolean 类型,而目标对象类型是 String 类型,这种情况可以通过 @Mapper 的 uses 属性来实现:

PersonDTO 类:

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Boolean isMarried;
}
1
2
3
4
5
6
7
8

PersonVO 类

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private String isMarried;
}
1
2
3
4
5
6
7
8

PersonConvert 转换类,使用 uses 属性来指定不同类型的转换

@Mapper(uses = {BooleanStrFormat.class})
public interface PersonConvert {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
    
    PersonVO personVOToDTO(PersonDTO personDTO);

}
1
2
3
4
5
6
7

BooleanStrFormat 类

public class BooleanStrFormat {
    public String toStr(Boolean isMarried) {
        return isMarried ? "已婚" : "未婚";
    }
}
1
2
3
4
5

运行:

public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setIsMarried(true);
        PersonVO personVO = PersonConvert.INSTANCE.personVOToDTO(personDTO);
        System.out.println("转换成功的 personVO:" + personVO);
    }
}
1
2
3
4
5
6
7
8

打印:

转换成功的 personVO:PersonVO(isMarried=已婚)
1

可以知道 DTO 的 Boolean 经过 BooleanStrFormat 转换,得出了 VO 的已婚。

要注意的是,如果使用了例如像 Spring 这样的环境 @Mapper(componentModel = "spring"),Mapper 用 uses 引入类实例的方式也必须是自动注入,即这个类也应该纳入 Spring 容器。

# 封装公共映射类

基于上面的使用,我们可以封装一个公共的映射类

public interface BaseMapperConvertor<S, T> {
    
    T convert(S sourceClass);
    
    S convertInvert(T targetClass);

    List<T> convert(List<S> sourceClass);

    List<S> convertInvert(List<T> targetClass);
    
    void update(S sourceClass, @MappingTarget T targetClass);
    
    void updateInvert(T targetClass, @MappingTarget S sourceClass);
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这是最基本的封装类,仅限 Source 类和 Target 类的属性类型和属性名一致。

使用:

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonDTO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PersonVO {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
    private Date birthDate;
    private Boolean isMarried;
}

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

PersonDTO 和 PersonVO 的属性类型和属性名一样,则定义一个 MapStruct 类继承 BaseMapper

@Mapper
public interface PersonConvert extends BaseMapperConvertor<PersonDTO, PersonVO> {
    PersonConvert INSTANCE = Mappers.getMapper(PersonConvert.class);
}
1
2
3
4

运行类:











 





public class Main {
    public static void main(String[] args) {
        PersonDTO personDTO = new PersonDTO();
        personDTO.setId(127);
        personDTO.setName("可乐");
        personDTO.setAge(24);
        personDTO.setAddress("深圳");
        personDTO.setBirthDate(new Date());
        personDTO.setIsMarried(true);
      
        PersonVO personVO = PersonConvert.INSTANCE.convert(personDTO);
      
        System.out.println("转换成功的 personVO:" + personVO);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

打印:

转换成功的 personVO:PersonVO(id=127, name=可乐, age=24, address=深圳, birthDate=Fri Jul 14 00:20:20 CST 2023, isMarried=true)
1

我们可以封装更多的映射类,这是基于自己业务需求来封装,如有很多类需要将 name 转成 personName,则可以自行基于 BaseMapper 进一步封装。

# 遵循原则

  • 当多个对象中, 有其中一个为 null, 则会直接返回 null
  • 如一对一转换一样, 属性通过名字来自动匹配。因此, 名称和类型相同的不需要进行特殊处理
  • 当多个原对象中,有相同名字的属性时,需要通过 @Mapping 注解来具体的指定, 以免出现歧义(不指定会报错)。如上面的 name
  • 属性也可以直接从传入的参数来赋值,如自定义转换规则和 @AfterMapping 修饰的方法修改属性值
更新时间: 2024/01/17, 05:48:13
最近更新
01
JVM调优
12-10
02
jenkins
12-10
03
Arthas
12-10
更多文章>