1. Overview
在本教程中,我们将探讨MapStruct的使用,简单地说,它是一个Java Bean映射器。
这个API包含了两个Java Bean之间自动映射的函数。使用MapStruct,我们只需要创建接口,库将在编译时自动创建具体的实现。
2. MapStruct and Transfer Object Pattern
对于大多数应用,你会发现很多样板代码将POJO类转换为其他POJO类。
例如,一种常见的转换类型发生在持久化支持的Entity和到客户端的DTO之间。
因此,这就是MapStruct解决的问题:手动创建bean mapper是耗时的。但是该库可以自动生成bean mapper类。
3. Maven
让我们把下面的依赖项添加到我们的 Maven pom.xml 中:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.0.Beta1</version>
</dependency>
MapStruct 的最新稳定版本可以从 Maven Central Repository 获得。
我们还将 annotationProcessorPaths 部分添加到 maven-compiler-plugin 插件的配置部分。
mapstruct-processor 用于在构建期间生成映射器实现:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.0.Beta1</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
4. Basic Mapping
4.1. Creating a POJO
我们首先创建一个POJO类:
public class SimpleSource {
private String name;
private String description;
// getters and setters
}
public class SimpleDestination {
private String name;
private String description;
// getters and setters
}
4.2. The Mapper Interface
@Mapper
public interface SimpleSourceDestinationMapper {
SimpleDestination sourceToDestination(SimpleSource source);
SimpleSource destinationToSource(SimpleDestination destination);
}
注意:我们并没有创建SimpleSourceDestinationMapper接口的实现类,因为MapStruct将自动为我们创建。
4.3. The New Mapper
MapStruct的处理过程可以通过mvn clean install触发,这个命令执行后将在/target/generated-sources/annotations/目录下生成一个实现类。
以下是MapStruct 自动生成的实现类:
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;
}
}
4.4. Test Case
写一个测试用例用于测试SimpleSource类和SimpleDestination类之间的转换:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SimpleSourceDestinationMapperIntegrationTest {
@Autowired
SimpleSourceDestinationMapper simpleSourceDestinationMapper;
@Test
public void givenSourceToDestination_whenMaps_thenCorrect() {
SimpleSource simpleSource = new SimpleSource();
simpleSource.setName("SourceName");
simpleSource.setDescription("SourceDescription");
SimpleDestination destination = simpleSourceDestinationMapper.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 = simpleSourceDestinationMapper.destinationToSource(destination);
assertEquals(destination.getName(), source.getName());
assertEquals(destination.getDescription(), source.getDescription());
}
}
5. Mapping With Dependency Injection
接下来,我们仅仅通过调用Mappers.getMapper(YourClass.class)来获得一个mapper实例。
当然,这是一个非常典型的通过手动调用获取实例的方式,一个更好的实践是通过依赖注入的方式来直接使用mapper。
幸运的是,MapStruct已经支持了spring的依赖注入。
在mapper中使用spring的控制反转,我们需要在component类上使用@Mapper注解。
5.1. Modify the Mapper
在SimpleSourceDestinationMapper类上添加以下代码:
@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper
5.2. Inject Spring Components into the Mapper
有时候,我们需要在mapper的实现逻辑里面使用Spring components。
在这里,我们使用一个抽象类来替代接口:
@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService
这样,我们就可以使用@Autowired 注解在这个抽象类中注入所需的component。
@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService {
@Autowired
protected SimpleService simpleService;
@Mapping(target = "name", expression = "java(simpleService.enrichName(source.getName()))")
public abstract SimpleDestination sourceToDestination(SimpleSource source);
}
我们必须将注入的bean设置为私有,因为MapStruct会通过生成的实现来访问对象。
6. Mapping Fields With Different Field Names
通过前面的例子,MapStruct有可以自动将bean进行转换,因为这些bean具有相同名字的字段,如果我们想把不同名字的字段进行转换呢?
在下面的例子,我们将创建Employee和EmployeeDTO两个bean
6.1. New POJOs
public class EmployeeDTO {
private int employeeId;
private String employeeName;
// getters and setters
}
public class Employee {
private int id;
private String name;
// getters and setters
}
6.2. The Mapper Interface
当映射不同名称的字段时,我们需要配置source字段和target字段,这一过程需要在字段上添加@Mapping注解。
在MapStruct中,我们也可以使用‘点’符号来定义一个bean的成员字段。
@Mapper
public interface EmployeeMapper {
@Mapping(target = "employeeId", source = "entity.id")
@Mapping(target = "employeeName", source = "entity.name")
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mapping(target = "id", source = "dto.employeeId")
@Mapping(target = "name", source = "dto.employeeName")
Employee employeeDTOtoEmployee(EmployeeDTO dto);
}
6.3. Test Case
再次,通过简单的测试用例来验证:
@Test
public void givenEmployeeDTOwithDiffNametoEmployee_whenMaps_thenCorrect() {
EmployeeDTO dto = new EmployeeDTO();
dto.setEmployeeId(1);
dto.setEmployeeName("John");
Employee entity = mapper.employeeDTOtoEmployee(dto);
assertEquals(dto.getEmployeeId(), entity.getId());
assertEquals(dto.getEmployeeName(), entity.getName());
}
7. Mapping Beans With Child Beans
对于bean中持有的其他bean的引用,如何进行映射转换,这里的概念类似于深拷贝。
7.1. Modify the POJO
我们在前面的EmployeeDTO 类和Employee类中分别添加一个对象成员:
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
}
7.2. Modify the Mapper
我们需要添加一个方法去转换Division类和DivisionDTO类,反之亦然。
如果MapStruct检测到对象类型需要转换,并且转换方法存在在同一个类中,他将会自动完成转换的实现逻辑。
我们在mapper中添加以下方法声明:
DivisionDTO divisionToDivisionDTO(Division entity);
Division divisionDTOtoDivision(DivisionDTO dto);
7.3. Modify the Test Case
在前面的测试用例中,稍作修改:
@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());
}
8. Mapping With Type Conversion
MapStruct同样也提供了一对开箱即用的隐式转换器,下面我们尝试着转化String类型的日期到一个真正的Date对象。
关于更多的隐式类型转换,请参考官方文档https://mapstruct.org/documentation/stable/reference/html/
8.1. Modify the Beans
在employee曾加一个开始日期:
public class Employee {
// other fields
private Date startDt;
// getters and setters
}
public class EmployeeDTO {
// other fields
private String employeeStartDt;
// getters and setters
}
8.2. Modify the Mapper
修改mapper,并提供dateFormat给日期:
@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);
@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);
8.3. Modify the Test Case
再次修改测试用例:
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());
}
9. Mapping With an Abstract Class
有时候,我们需要对mapper的定制,已经超出了@Mapping注解所提供的能力范围。
例如额外的类型转换,有时需要将值进行变形,参考接下来的例子。
在这些例子里,我们创建了一些抽象类,并实现了其中的一些我们需要定制的方法,并把那些MapStruct可以自动生成的方法声明称抽象的。
9.1. Basic Model
接下来的类型例子:
public class Transaction {
private Long id;
private String uuid = UUID.randomUUID().toString();
private BigDecimal total;
//standard getters
}
对应的DTO类:
public class TransactionDTO {
private String uuid;
private Long totalInCents;
// standard getters and setters
}
需要注意的部分是,我们将总钱数从BigDecimal total 转换成Long totalInCents。
9.2. Defining a Mapper
通过创建一个Mapper抽象类来实现目的。
@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将会替我们实现。
9.3. Generated Result
当我们已经实现了一个将Transaction类型转换成TransactionDTO类型的方法,我们希望MapStruct在其他方法中使用它。
@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;
}
}
上面,list.add( toTransactionDTO( transaction ) );
中MapStruct 使用了我们的实现类进行生成转换后的bean.
10. Before-Mapping and After-Mapping Annotations
还有另一种方法,用于定制化@Mapping注释的功能,就是通过使用 @BeforeMapping 和 @AfterMapping, 这两个注释用标注于围绕着bean对象转换逻辑的前面和后面的执行方法。
在一些需要将这些特殊行为应用于超类的场景下,将会带来极大便利。
下面的例子展示了,将继承自Car的ElectricCar和BioDieselCar类型转换成CarDTO。
10.1. Basic Model
public class Car {
private int id;
private String name;
}
继承类:
public class BioDieselCar extends Car {
}
public class ElectricCar extends Car {
}
CarDTO 中包含了一个枚举类型FuelType的字段:
public class CarDTO {
private int id;
private String name;
private FuelType fuelType;
}
public enum FuelType {
ELECTRIC, BIO_DIESEL
}
10.2. Defining the Mapper
接下来实现mapper抽象类,用于将Car映射到CarDTO
@Mapper
public abstract class CarsMapper {
@BeforeMapping
protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
if (car instanceof ElectricCar) {
carDto.setFuelType(FuelType.ELECTRIC);
}
if (car instanceof BioDieselCar) {
carDto.setFuelType(FuelType.BIO_DIESEL);
}
}
@AfterMapping
protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
carDto.setName(carDto.getName().toUpperCase());
}
public abstract CarDTO toCarDto(Car car);
}
@MappingTarget 是一个作用在方法参数的注解,用于将在映射逻辑执行前后能够准确地获取到DTO对象。
10.3. Result
前面通过生成器实现的CarsMapper:
@Generated
public class CarsMapperImpl extends CarsMapper {
@Override
public CarDTO toCarDto(Car car) {
if (car == null) {
return null;
}
CarDTO carDTO = new CarDTO();
enrichDTOWithFuelType(car, carDTO);
carDTO.setId(car.getId());
carDTO.setName(car.getName());
convertNameToUpperCase(carDTO);
return carDTO;
}
}
注意映射逻辑的实现的调用方式
11. Support for Lombok
在最近版本的MapStruct中,已经支持Lombok的使用,我们可以很容易的在映射过程中使用Lombok。
在Lombok 1.18.16版本之后,我们需要在lombok-mapstruct-binding添加依赖,下面是maven编译插件的示例:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
用Lombok的方式定义源entity:
@Getter
@Setter
public class Car {
private int id;
private String name;
}
需要转换的目标实体:
@Getter
@Setter
public class CarDTO {
private int id;
private String name;
}
mapper接口和我们之前写的类似:
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
CarDTO carToCarDTO(Car car);
}
12. Support for defaultExpression
从version 1.3.0开始,当源字段的值为null时,我们可以在@Mapping注解中使用defaultExpression,用于指定一个表达式,来明确目标字段的默认值。
源entity:
public class Person {
private int id;
private String name;
}
目标entity:
public class PersonDTO {
private int id;
private String name;
}
如果,源entity的ID字段的值是null, 我们想生成一个随机的ID,并赋值给目标entity:
@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);
}
让我们用单元测试来测一下:
@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());
}
13. Conclusion
本文对MapStruct进行了介绍,我们对Mapping功能类库的基本使用方法进行了展示。
对于前面的例子和单元测试的代码,都可以在github项目中找到,https://github.com/eugenp/tutorials/tree/master/mapstruct