MapStruct使用

我们在实际开发过程中会出现很多bean之间的拷贝动作,这样的动作需要不停的去进行set操作。容易造成代码的耦合和维护困难。通过MapStruct我们只需要定义一个接口,这个工具包会帮我们自动生成一个实现类,并且这个类是不可编辑的,代码自动生成帮我们省去了开发维护成本同时做了一定的解耦。

Maven依赖

使用MapStruct我们需要通过maven导入相关的依赖:

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

同时我们需要添加maven-compiler-plugin,其中mapstruct-processor用于构建mapper实现类的生成器。

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>

基本的Mapping用法

定义一个Java的POJO类和Mapper Interface:

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
}
1
2
3
4
5
@Mapper
public interface SimpleSourceDestinationMapper {
SimpleDestination sourceToDestination(SimpleSource source);
SimpleSource destinationToSource(SimpleDestination destination);
}

注意到我们这里并没有去实现这个接口,但是MapStruct帮我们生成了一个实现类。我们可以通过调用mvn compile或者通过mvn clean install调用MapStruct,它会帮我们生成一个Mapper的实现类在/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;
}
}

执行一个测试用例:

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());
}
}

通过依赖注入的方式获取Mapper服务

我们一般情况下可以通过Mappers.getMapper(YourClass.class)的方式获取到一个Mapper实例,然后调用到具体的mapper方法,我们也可以注入的方式获取到我们的实例:

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

在interface上加了这个属性之后,我们可以通过@Autowire的方式在任何地方引用这个实例。

映射不同的字段名

针对下面的POJOs我们可以定义另一个Mapper:

1
2
3
4
5
6
7
8
9
10
public class EmployeeDTO {
private int employeeId;
private String employeeName;
// getters and setters
}
public class Employee {
private int id;
private String name;
// getters and setters
}
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);
}

映射对象字段

如果这边两个类之间的映射中还有嵌套的对象,我们在同一个Mapper的接口中定义这两个嵌套对象的转换规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class EmployeeDTO {
private int employeeId;
private String employeeName;
private DivisionDTO division;
// getters and setters omitted
}
public class Employee {
private int id;
private String name;
private Division division;
// getters and setters omitted
}
public class Division {
private int id;
private String name;
// default constructor, getters and setters omitted
}

修改之后的Mapper接口为:

1
2
3
DivisionDTO divisionToDivisionDTO(Division entity);

Division divisionDTOtoDivision(DivisionDTO dto);

测试用例:

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());
}

字段类型转换

如果我们需要将一个String类型映射为Date类型的字段,定义一个dateFormat,同时还提供了expression等属性:

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);

@Mappping的注解我们还可以定义一些属性,defaultExpression和expression可以定义一些简单的表达式,直接传递给实现方法,但是不可能引用一些工具类,只要import的方法都没办法自动导入,这个比较鸡肋。这里贴一些简单的:

1
2
3
4
5
6
7
8
9
//这个会将java.util.UUID.randomUUID().toString()这个方法直接引用在目标字段上。
@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
10
11
12
13
14
15
16
17
18
19
20
@Mapper(componentModel = "spring")
public interface EmployeeMapper {
EmployeeMapper INSTANCE= Mappers.getMapper(EmployeeMapper.class);
@Mappings({
@Mapping(target="employeeId", source="entity.id"),
@Mapping(target="employeeName", source="entity.name"),
@Mapping(target = "testString",expression = "java((priceFen/100) + \"元\")")
})
EmployeeDTO employeeToEmployeeDTO(Employee entity,Long priceFen);
@Mappings({
@Mapping(target="id", source="dto.employeeId"),
@Mapping(target="name", source="dto.employeeName")
})
Employee employeeDTOtoEmployee(EmployeeDTO dto);
@Mappings({
@Mapping(target = "idDTO",source = "division.id"),
@Mapping(target = "nameDTO",source = "division.name")
})
DivisionDTO divisionToDivisionDTO(Division division);
}

其实这个感觉还是比较鸡肋的,如果可以支持比较复杂的一些转换实际场景可能更受用。

进行复杂转换

主要在实际的应用场景中,需要输入转换的参数可能是多个,同时我们还需要一些自定义的部分,通过加@BeforeMapping等注解可以将自定义的部分嵌入到实现类中,这里也可以支持lambok,有些版本可能会报错,maven的版本需要在3.6.0以上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//两个POJOs
@Data
public class TransactionDTO {

private String uuid;
private Long totalInCents;

// standard getters and setters
}
@Data
public class Transaction {
private Long id;
private String uuid = UUID.randomUUID().toString();
private BigDecimal total;

//standard getters
}

需要定义为抽象类,这里的寓意是限制性第一个方法,在执行第二个方法,第一个方法中可以加一些复杂的参数转换,第二个方法做一些通用的处理,同时需要使用@MappingTarget注解保证输出对象的唯一引用,不加默认会帮你重新new一个出来并且return:

1
2
3
4
5
6
7
8
9
10
@Mapper(componentModel = "spring")
public abstract class TransactionMapper {
@BeforeMapping()
public void toTransactionDTO(Transaction transaction,@MappingTarget TransactionDTO transactionDTO) {
transactionDTO.setTotalInCents(transaction.getTotal()
.multiply(new BigDecimal("100")).longValue());
}
@Mapping(source = "transaction.uuid",target = "uuid")
public abstract void transactionToTransactionDTO(Transaction transaction , @MappingTarget TransactionDTO transactionDTO);
}

总结

这里为什么要介绍MapStruct这个工具包,实际上在我们的项目中确实有太多的拷贝动作,导致了方法都是在set,get方法过长,较难进行维护,非常不清晰。之后可以尝试通过MapStruct改造一些有类似问题的接口,通过定义一个抽象类的Mapper,将复杂操作维护一个自定义方法,简单的映射直接通过@Mapping,开发者只需要关心对应的抽象接口定义。