MapStruct 介绍

1. 概述

在一个成熟的工程中,尤其是现在的分布式系统中,应用与应用之间,还有单独的应用细分模块之后,DO 一般不会让外部依赖,这时候需要在提供对外接口的模块里放 DTO 用于对象传输,也即是 DO 对象对内,DTO对象对外,DTO 可以根据业务需要变更,并不需要映射 DO 的全部属性。

这种 对象与对象之间的互相转换,就需要有一个专门用来解决转换问题的工具,毕竟每一个字段都 get/set 会很麻烦。

MapStruct 就是这样的一个属性映射工具,只需要定义一个 Mapper 接口,MapStruct 就会自动实现这个映射接口,避免了复杂繁琐的映射实现。MapStruct官网地址: mapstruct.org

2. Maven

让我们在Maven pom.xml中添加以下依赖项:

1
2
3
4
5
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>1.3.0.Beta2</version>
</dependency>

最新的Mapstruct及其processor稳定版本均可从Maven Central Repository获得。

我们还将annotationProcessorPaths部分添加到maven-compiler-plugin插件的配置部分,用于生成具体的映射器实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.0.Beta2</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

3. 基本映射

3.1 创建基本的 POJO

让我们首先创建一个简单的Java POJO:

1
2
3
4
5
6
7
8
9
10
11
public class SimpleSource {
private String name;
private String description;
// getters and setters
}

public class SimpleDestination {
private String name;
private String description;
// getters and setters
}

3.2. 映射器接口

1
2
3
4
5
@Mapper
public interface SimpleSourceDestinationMapper {
SimpleDestination sourceToDestination(SimpleSource source);
SimpleSource destinationToSource(SimpleDestination destination);
}

请注意,我们没有为SimpleSourceDestinationMapper创建实现类- 因为MapStruct将为我们创建它。

3.3. 映射器接口实现

我们可以通过执行mvn clean install来触发MapStruct处理,插件将在 /target/generated-sources/annotations/下生成具体的实现类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SimpleSourceDestinationMapperImpl
implements SimpleSourceDestinationMapper {
@Override
public SimpleDestination sourceToDestination(SimpleSource source) {
if ( source == null ) {
return null;
}
SimpleDestination simpleDestination = new SimpleDestination();
simpleDestination.setName( source.getName() );
simpleDestination.setDescription( source.getDescription() );
return simpleDestination;
}
@Override
public SimpleSource destinationToSource(SimpleDestination destination){
if ( destination == null ) {
return null;
}
SimpleSource simpleSource = new SimpleSource();
simpleSource.setName( destination.getName() );
simpleSource.setDescription( destination.getDescription() );
return simpleSource;
}
}

3.4 测试用例

最后,通过生成所有内容,让我们编写一个测试用例,将显示SimpleSource中的值与SimpleDestination中的值匹配:

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
public class SimpleSourceDestinationMapperTest {
private SimpleSourceDestinationMapper mapper
= Mappers.getMapper(SimpleSourceDestinationMapper.class);
@Test
public void givenSourceToDestination_whenMaps_thenCorrect() {
SimpleSource simpleSource = new SimpleSource();
simpleSource.setName("SourceName");
simpleSource.setDescription("SourceDescription");
SimpleDestination destination = mapper.sourceToDestination(simpleSource);

assertEquals(simpleSource.getName(), destination.getName());
assertEquals(simpleSource.getDescription(),
destination.getDescription());
}
@Test
public void givenDestinationToSource_whenMaps_thenCorrect() {
SimpleDestination destination = new SimpleDestination();
destination.setName("DestinationName");
destination.setDescription("DestinationDescription");
SimpleSource source = mapper.destinationToSource(destination);
assertEquals(destination.getName(), source.getName());
assertEquals(destination.getDescription(),
source.getDescription());
}
}

4. 使用 Spring 依赖注入

在上面的单元测试中,我们通过调用Mappers.getMapper(YourClass.class)来获取MapStruct中mapper的实例。当然,这是获取实例的一种非常手动的方式 - 更好的替代方法是将mapper直接注入我们需要的位置(如果我们的项目使用任何依赖注入解决方案)。

幸运的是,MapStruct对Spring和CDI上下文和依赖注入都提供了很好的支持

要在我们的映射器中使用Spring IoC,只需要给@Mapper注解添加 componentModel=spring 属性,如下所示:

1
2
@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper

5. 使用不同字段名称映射字段

从前面的示例中,MapStruct能够自动映射我们的bean,因为它们具有相同的字段名称。那么如果我们要映射的bean有不同的字段名称呢?

对于我们的示例,我们将创建一个名为EmployeeEmployeeDTO的新bean 。

5.1 新的 POJO

1
2
3
4
5
public class EmployeeDTO {
private int employeeId;
private String employeeName;
// getters and setters
}
1
2
3
4
5
public class Employee {
private int id;
private String name;
// getters and setters
}

5.2 Mapper

在映射不同的字段名称时,我们需要将其源字段配置为其目标字段,为此,我们需要添加@Mappings注释。此批注接受一个@Mapping批注数组,我们将使用它来添加目标和源属性。

在MapStruct中,我们还可以使用点表示法来定义bean的成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper
public interface EmployeeMapper {
@Mappings({
@Mapping(target="employeeId", source="entity.id"),
@Mapping(target="employeeName", source="entity.name")
})
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mappings({
@Mapping(target="id", source="dto.employeeId"),
@Mapping(target="name", source="dto.employeeName")
})
Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

5.3 测试用例

我们再次需要测试源和目标对象值是否匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper
public interface EmployeeMapper {
@Mappings({
@Mapping(target="employeeId", source="entity.id"),
@Mapping(target="employeeName", source="entity.name")
})
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mappings({
@Mapping(target="id", source="dto.employeeId"),
@Mapping(target="name", source="dto.employeeName")
})
Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

6. 嵌套映射

接下来,我们将展示如何使用对其他bean的引用来映射bean。

6.1 修改 POJO

让我们为Employee对象添加一个新的bean引用:

1
2
3
4
5
6
public class EmployeeDTO {
private int employeeId;
private String employeeName;
private DivisionDTO division;
// getters and setters omitted
}
1
2
3
4
5
6
public class Employee {
private int id;
private String name;
private Division division;
// getters and setters omitted
}
1
2
3
4
5
public class Division {
private int id;
private String name;
// default constructor, getters and setters omitted
}

6.2 修改Mapper

这里我们需要添加一个方法将Division转换为DivisionDTO,反之亦然; 如果MapStruct检测到需要转换对象类型并且转换方法存在于同一个类中,那么它将自动使用它。

让我们将它添加到映射器:

1
2
3
DivisionDTO divisionToDivisionDTO(Division entity);

Division divisionDTOtoDivision(DivisionDTO dto);

6.3 修改测试用例

让我们修改并添加一些测试用例到现有测试用例:

1
2
3
4
5
6
7
8
9
10
@Test
public void givenEmpDTONestedMappingToEmp_whenMaps_thenCorrect() {
EmployeeDTO dto = new EmployeeDTO();
dto.setDivision(new DivisionDTO(1, "Division1"));
Employee entity = mapper.employeeDTOtoEmployee(dto);
assertEquals(dto.getDivision().getId(),
entity.getDivision().getId());
assertEquals(dto.getDivision().getName(),
entity.getDivision().getName());
}

7. 映射过程中的类型转换

MapStruct还提供了几个现成的隐式类型转换,对于我们的示例,我们将尝试将String日期转换为实际的Date对象。

有关隐式类型转换的更多详细信息,可以阅读MapStruct参考指南

7.1 修改 POJO

1
2
3
4
5
public class Employee {
// other fields
private Date startDt;
// getters and setters
}
1
2
3
4
5
public class EmployeeDTO {
// other fields
private String employeeStartDt;
// getters and setters
}

7.2 修改Mapper

修改映射器并为我们的开始日期提供dateFormat

1
2
3
4
5
6
7
8
9
10
11
12
@Mappings({
@Mapping(target="employeeId", source = "entity.id"),
@Mapping(target="employeeName", source = "entity.name"),
@Mapping(target="employeeStartDt", source = "entity.startDt",
dateFormat = "dd-MM-yyyy HH:mm:ss")})
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mappings({
@Mapping(target="id", source="dto.employeeId"),
@Mapping(target="name", source="dto.employeeName"),
@Mapping(target="startDt", source="dto.employeeStartDt",
dateFormat="dd-MM-yyyy HH:mm:ss")})
Employee employeeDTOtoEmployee(EmployeeDTO dto);

7.3 修改测试用例

让我们再添加一些测试用例来验证转换是否正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final String DATE_FORMAT = "dd-MM-yyyy HH:mm:ss";
@Test
public void givenEmpStartDtMappingToEmpDTO_whenMaps_thenCorrect() throws ParseException {
Employee entity = new Employee();
entity.setStartDt(new Date());
EmployeeDTO dto = mapper.employeeToEmployeeDTO(entity);
SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);

assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
entity.getStartDt().toString());
}
@Test
public void givenEmpDTOStartDtMappingToEmp_whenMaps_thenCorrect() throws ParseException {
EmployeeDTO dto = new EmployeeDTO();
dto.setEmployeeStartDt("01-04-2016 01:00:00");
Employee entity = mapper.employeeDTOtoEmployee(dto);
SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);

assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
entity.getStartDt().toString());
}

8. 自定义映射结合(抽象类)

有时,我们希望通过自定义一些映射方式来扩展 @Mapping功能,比如除了类型转换之外,我们可能希望以某种方式转换值,如下面的示例所示,在这种情况下,我们可以创建一个抽象类并实现我们想要自定义的方法,并留下那些应该由MapStruct生成的抽象类。

8. 1 基本模型

在这个例子中,我们将使用以下类:

1
2
3
4
5
6
7
public class Transaction {
private Long id;
private String uuid = UUID.randomUUID().toString();
private BigDecimal total;

//standard getters
}

匹配的 DTO:

1
2
3
4
5
6
7
public class TransactionDTO {

private String uuid;
private Long totalInCents;

// standard getters and setters
}

这里棘手的部分是将BigDecimal 金额转换为Long totalInCents

8.2 定义 Mapper

我们可以通过将Mapper 创建 为抽象类来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper
abstract class TransactionMapper {

public TransactionDTO toTransactionDTO(Transaction transaction) {
TransactionDTO transactionDTO = new TransactionDTO();
transactionDTO.setUuid(transaction.getUuid());
transactionDTO.setTotalInCents(transaction.getTotal()
.multiply(new BigDecimal("100")).longValue());
return transactionDTO;
}

public abstract List<TransactionDTO> toTransactionDTO(Collection<Transaction> transactions);
}

在这里,我们为单个对象转换实现了完全自定义的映射方法。

另一方面,我们留下了将Collection映射到List抽象的方法,因此MapStruct 将为我们实现它。

8.3 生成的结果

由于我们已经实现了将单个Transaction映射到TransactionDTO的方法,我们希望Mapstruct在第二种方法中使用它。将生成以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Generated
class TransactionMapperImpl extends TransactionMapper {

@Override
public List<TransactionDTO> toTransactionDTO(Collection<Transaction> transactions) {
if ( transactions == null ) {
return null;
}

List<TransactionDTO> list = new ArrayList<>();
for ( Transaction transaction : transactions ) {
list.add( toTransactionDTO( transaction ) );
}

return list;
}
}

正如我们在第12行中看到的,MapStruct 在它生成的方法中使用我们的实现。

9. 支持 Lombok

在最新版本的MapStruct中,已经提供了对Lombok的支持。因此,我们可以使用Lombok轻松映射源实体和目标。 要启用Lombok支持,我们需要在注释处理器路径中添加依赖项。所以现在我们在Maven编译器插件中有了mapstruct-processor和Lombok:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.0.Beta2</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

让我们使用Lombok注释定义源实体:

1
2
3
4
5
6
@Getter
@Setter
public class Car {
private int id;
private String name;
}

和目标数据传输对象:

1
2
3
4
5
6
@Getter
@Setter
public class CarDTO {
private int id;
private String name;
}

这个mapper接口仍然类似于我们前面的例子:

1
2
3
4
5
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
CarDTO carToCarDTO(Car car);
}

10. 支持 defaultExpression

从版本1.3.0开始,我们可以使用@Mapping批注的defaultExpression属性来指定一个表达式,如果源字段为null,则该表达式确定目标字段的值。这是现有defaultValue属性功能的补充。

源实体:

1
2
3
4
public class Person {
private int id;
private String name;
}

目标数据传输对象:

1
2
3
4
public class PersonDTO {
private int id;
private String name;
}

如果源实体的id字段为null,我们想要生成一个随机id并将其分配给目标,保持其他属性值为:

1
2
3
4
5
6
7
8
@Mapper
public interface PersonMapper {
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

@Mapping(target = "id", source = "person.id",
defaultExpression = "java(java.util.UUID.randomUUID().toString())")
PersonDTO personToPersonDTO(Person person);
}

让我们添加一个测试用例来验证表达式的执行:

1
2
3
4
5
6
7
8
9
@Test
public void givenPersonEntitytoPersonWithExpression_whenMaps_thenCorrect()
Person entity = new Person();
entity.setName("Micheal");
PersonDTO personDto = PersonMapper.INSTANCE.personToPersonDTO(entity);
assertNull(entity.getId());
assertNotNull(personDto.getId());
assertEquals(personDto.getName(), entity.getName());
}