在本教程中,我们将探讨 MapStruct 的使用,简单地说,它就是 Java Bean 映射器。该 API 包含在两个 Java Bean 之间自动映射的函数。使用 MapStruct,我们只需创建接口,该库就会在编译时自动创建具体实现。
对于大多数应用程序来说,你会发现有很多将 POJO 转换为其他 POJO 的模板代码。例如,在持久化支持的实体和客户端的 DTO 之间会发生一种常见的转换。因此,这就是 MapStruct 所要解决的问题: 手动创建 Bean 映射器非常耗时。但该库可以自动生成 Bean 映射器类。
让我们先创建一个简单的 Java POJO:
public class SimpleSource {
private String name;
private String description;
}
public class SimpleDestination {
private String name;
private String description;
}
@Mapper
public interface SimpleSourceDestinationMapper {
SimpleDestination sourceToDestination(SimpleSource source);
SimpleSource destinationToSource(SimpleDestination destination);
}
请注意,我们没有为 SimpleSourceDestinationMapper 创建实现类,因为 MapStruct 会为我们创建它。
下面是 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;
}
}
最后,一切都已生成,让我们编写一个测试用例,显示 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());
}
}
接下来,让我们通过调用 Mappers.getMapper(YourClass.class) 来获取 MapStruct 中映射器的实例。当然,这是一种非常手动的获取实例的方法。不过,更好的方法是直接在需要的地方注入映射器(如果我们的项目使用任何依赖注入解决方案的话)。 幸运的是,MapStruct 对 Spring 和 CDI(上下文和依赖注入)都有可靠的支持。 要在映射器中使用 Spring IoC,我们需要在 @Mapper 中添加 componentModel 属性,值为 spring,而对于 CDI,则为 cdi。
在 SimpleSourceDestinationMapper 中添加以下代码:
@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper
有时,我们需要在映射逻辑中使用其他 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 必须访问生成的实现类中的对象。
在我们之前的示例中,MapStruct 能够自动映射我们的 Bean,因为它们具有相同的字段名。那么,如果我们要映射的 Bean 有不同的字段名呢?
在本例中,我们将创建一个名为 Employee 和 EmployeeDTO 的新 Bean。
public class EmployeeDTO {
private int employeeId;
private String employeeName;
}
public class Employee {
private int id;
private String name;
}
在映射不同的字段名时,我们需要将源字段配置为目标字段,为此,我们需要为每个字段添加 @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);
}
同样,我们需要测试源对象和目标对象的值是否匹配:
@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());
}
接下来,我们将展示如何将一个 Bean 映射到其他 Bean。
让我们为 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;
}
在这里,我们需要添加一个方法,将 Division 转换为 DivisionDTO,反之亦然;如果 MapStruct 检测到需要转换的对象类型和转换方法存在于同一个类中,它将自动使用该方法。
让我们将其添加到映射器中:
DivisionDTO divisionToDivisionDTO(Division entity);
Division divisionDTOtoDivision(DivisionDTO dto);
让我们修改并添加一些测试用例到现有的测试用例中:
@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());
}
MapStruct 还提供了一些现成的隐式类型转换,在我们的示例中,我们将尝试把字符串日期转换为实际的日期对象。
我们为员工添加一个开始日期:
public class Employee {
private Date startDt;
}
public class EmployeeDTO {
private String employeeStartDt;
}
我们修改映射器,为开始日期提供 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);
让我们再添加几个测试用例来验证转换是否正确:
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());
}
有时,我们可能希望以超出 @Mapping 能力的方式自定义映射器。 例如,除了类型转换,我们可能还想以某种方式转换值,就像下面的例子一样。 在这种情况下,我们可以创建一个抽象类并实现我们想要定制的方法,而将那些应由 MapStruct 生成的方法抽象出来。
在本例中,我们将使用以下类:
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。
我们可以通过将映射器创建为抽象类来实现这一点:
@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 将为我们实现该方法。
由于我们已经实现了将单个事务映射到 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 在生成的方法中使用了我们的实现。
这里还有另一种自定义 @Mapping 功能的方法,即使用 @BeforeMapping 和 @AfterMapping 注释。这些注解用于标记在映射逻辑之前和之后调用的方法。
在我们希望将此行为应用于所有映射超类的情况下,这些注解非常有用。
让我们来看一个将 Car ElectricCar 和 BioDieselCar 的子类型映射到 CarDTO 的示例。
在映射时,我们希望将类型的概念映射到 DTO 中的 FuelType 枚举字段。映射完成后,我们希望将 DTO 的名称改为大写。
我们将使用以下类:
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
}
现在,让我们继续编写将 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。
上面定义的 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;
}
}
请注意注释的方法调用是如何围绕实现中的映射逻辑的。
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);
}
从 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());
}
原文: