Spring MapStruct 快速指南

1. 概述

在本教程中,我们将探讨 MapStruct 的使用,简单地说,它就是 Java Bean 映射器。该 API 包含在两个 Java Bean 之间自动映射的函数。使用 MapStruct,我们只需创建接口,该库就会在编译时自动创建具体实现。

2. MapStruct 和传输对象模式

对于大多数应用程序来说,你会发现有很多将 POJO 转换为其他 POJO 的模板代码。例如,在持久化支持的实体和客户端的 DTO 之间会发生一种常见的转换。因此,这就是 MapStruct 所要解决的问题: 手动创建 Bean 映射器非常耗时。但该库可以自动生成 Bean 映射器类。

3. 基础映射

3.1. 创建POJO

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

public class SimpleSource {
    private String name;
    private String description;
    
}
 
public class SimpleDestination {
    private String name;
    private String description;
    
}

3.2. 映射器接口

@Mapper
public interface SimpleSourceDestinationMapper {
    SimpleDestination sourceToDestination(SimpleSource source);
    SimpleSource destinationToSource(SimpleDestination destination);
}

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

3.3. 新的映射器

下面是 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;
    }
}

3.4. 测试验证

最后,一切都已生成,让我们编写一个测试用例,显示 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());
    }

}

4. 利用依赖注入进行映射

接下来,让我们通过调用 Mappers.getMapper(YourClass.class) 来获取 MapStruct 中映射器的实例。当然,这是一种非常手动的获取实例的方法。不过,更好的方法是直接在需要的地方注入映射器(如果我们的项目使用任何依赖注入解决方案的话)。 幸运的是,MapStruct 对 Spring 和 CDI(上下文和依赖注入)都有可靠的支持。 要在映射器中使用 Spring IoC,我们需要在 @Mapper 中添加 componentModel 属性,值为 spring,而对于 CDI,则为 cdi。

4.1. 修改映射器

在 SimpleSourceDestinationMapper 中添加以下代码:

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

4.2. 将 Spring 组件注入映射器

有时,我们需要在映射逻辑中使用其他 Spring 组件。在这种情况下,我们必须使用抽象类而不是接口:

@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService

然后,我们就可以使用众所周知的 @Autowired 注解轻松注入所需的组件,并在代码中使用它:

@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 必须访问生成的实现类中的对象。

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

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

在本例中,我们将创建一个名为 Employee 和 EmployeeDTO 的新 Bean。

5.1. 新POJO

public class EmployeeDTO {

    private int employeeId;
    private String employeeName;
    
}
public class Employee {

    private int id;
    private String name;
    
}

5.2. 映射器接口

在映射不同的字段名时,我们需要将源字段配置为目标字段,为此,我们需要为每个字段添加 @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);
}

5.3. 测试验证

同样,我们需要测试源对象和目标对象的值是否匹配:

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

6. 映射beans和子beans

接下来,我们将展示如何将一个 Bean 映射到其他 Bean。

6.1. 修改POJO

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

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    private DivisionDTO division;
    
}
public class Employee {
    private int id;
    private String name;
    private Division division;
    
}
public class Division {
    private int id;
    private String name;
    
}

6.2. 修改映射器

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

让我们将其添加到映射器中:

DivisionDTO divisionToDivisionDTO(Division entity);

Division divisionDTOtoDivision(DivisionDTO dto);

6.3. 修改测试用例

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

@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 还提供了一些现成的隐式类型转换,在我们的示例中,我们将尝试把字符串日期转换为实际的日期对象。

7.1. 修改Beans

我们为员工添加一个开始日期:

public class Employee {
    
    private Date startDt;
    
}
public class EmployeeDTO {
    
    private String employeeStartDt;
    
}

7.2. 修改映射器

我们修改映射器,为开始日期提供 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);

7.3. 修改测试用例

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

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. 基础Model

在本例中,我们将使用以下类:

public class Transaction {
    private Long id;
    private String uuid = UUID.randomUUID().toString();
    private BigDecimal total;

    
}

和一个匹配的 DTO:

public class TransactionDTO {

    private String uuid;
    private Long totalInCents;

    
}

这里最棘手的部分是将 BigDecimal 的美元总数转换为 Long totalInCents。

8.2. 定义映射器

我们可以通过将映射器创建为抽象类来实现这一点:

@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. 生成结果

由于我们已经实现了将单个事务映射到 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;
    }
}

我们可以在第 12 行看到,MapStruct 在生成的方法中使用了我们的实现。

9. 映射前和映射后注释

这里还有另一种自定义 @Mapping 功能的方法,即使用 @BeforeMapping 和 @AfterMapping 注释。这些注解用于标记在映射逻辑之前和之后调用的方法。

在我们希望将此行为应用于所有映射超类的情况下,这些注解非常有用。

让我们来看一个将 Car ElectricCar 和 BioDieselCar 的子类型映射到 CarDTO 的示例。

在映射时,我们希望将类型的概念映射到 DTO 中的 FuelType 枚举字段。映射完成后,我们希望将 DTO 的名称改为大写。

9.1. 基础Model

我们将使用以下类:

public class Car {
    private int id;
    private String name;
}

Car的子类型:

public class BioDieselCar extends Car {
}
public class ElectricCar extends Car {
}

带有枚举字段类型 FuelType 的 CarDTO:

public class CarDTO {
    private int id;
    private String name;
    private FuelType fuelType;
}
public enum FuelType {
    ELECTRIC, BIO_DIESEL
}

9.2. 定义映射器

现在,让我们继续编写将 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 是一个参数注解,在 @BeforeMapping 和 @AfterMapping 注解方法中,它分别在执行映射逻辑之前和之后填充目标映射 DTO。

9.3. 结果

上面定义的 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;
    }
}

请注意注释的方法调用是如何围绕实现中的映射逻辑的。

10. Lombok支持

MapStruct 的最新版本宣布支持 Lombok。因此,我们可以使用 Lombok 轻松地映射源实体和目标实体。

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

@Getter
@Setter
public class Car {
    private int id;
    private String name;
}

以及目的地数据传输对象:

@Getter
@Setter
public class CarDTO {
    private int id;
    private String name;
}

其映射器接口与我们之前的示例类似:

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

11. 支持_defaultExpression_

从 1.3.0 版开始,我们可以使用 @Mapping 注解的 defaultExpression 属性来指定一个表达式,以便在源字段为空时确定目标字段的值。这是现有 defaultValue 属性功能的补充。

源实体

public class Person {
    private int id;
    private String name;
}

目标数据传输对象:

public class PersonDTO {
    private int id;
    private String name;
}

如果源实体的 id 字段为空,我们会随机生成一个 id 并将其分配给目的地,其他属性值保持不变:

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

原文:

https://www.baeldung.com/mapstruct