https://www.baeldung.com/mapstruct # MapStruct 快速指南 # 1. 总览 在这个教程,我们将探索 MapStruct 的使用,简单来说也就是一个 Java Bean 映射工具。 这个 API 包括自动在两个 Java Bean 之间映射的函数。通过 MapStruct,我们只需要创建接口,库将在编译期自动创建具体实现。 # 2. MapStruct 和传输对象模式 对于大多数应用程序,您会注意到许多将 POJO 转换为其他 POJO 的样板代码。 例如,一种常见类型的转换发生在持久层支持的实体和到达客户端的 DTO 之间。 因此,这就是 MapStruct 解决的问题:手动创建 bean 映射器非常耗时。但是库**可以自动生成 bean 映射器类**。 # 3. Maven 让我们添加以下依赖到我们 Maven `pom.xml`: ```xml org.mapstruct mapstruct 1.5.3.Final ``` MapStruct 及其处理器的最新稳定版本均可从 Maven Central Repository 获得。 让我们将 `annotationProcessorPaths` 部分添加到 maven 编译器插件的配置部分。 `mapstruct-processor` 用于在生成过程中生成映射器实现: ```xml org.apache.maven.plugins maven-compiler-plugin 3.5.1 1.8 1.8 org.mapstruct mapstruct-processor 1.5.3.Final ``` # 4. 基本映射 ## 4.1 创建 POJO 让我们首先创建一个简单的 Java POJO: ```java 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 Mapper 接口 ```java @Mapper public interface SimpleSourceDestinationMapper { SimpleDestination sourceToDestination(SimpleSource source); SimpleSource destinationToSource(SimpleDestination destination); } ``` 注意,我们没有为 `SimpleSourceDestinationMapper` 创建实现类,因为 MapStruct 为我们创建了它。 ## 4.3 新 Mapper 我们可以通过执行 `mvn clean install` 来触发 MapStruct 处理。 这将在 `/target/generated sources/annotations/` 下生成实现类。 以下是 MapStruct 自动为我们创建的类: ```java 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 测试案例 最后,在生成所有内容后,让我们编写一个测试用例,显示 `SimpleSource` 中的值与 `SimpleDestination` 中的值匹配: ```java public class SimpleSourceDestinationMapperIntegrationTest { 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()); } } ``` # 5. 通过依赖注入的映射 接下来,让我们通过调用 `Mappers.getMapper(YourClass.class)` 来获取 MapStruct 中的映射器实例。 当然,这是一种非常手动的获取实例的方法。然而,更好的选择是直接在需要的地方注入映射器(如果我们的项目使用任何依赖注入解决方案)。 **幸运的是,MapStruct 支持 Spring 和 CDI**(上下文和依赖注入)。 要在映射器中使用 Spring IoC,我们需要将 `componentModel` 属性添加到 `@Mapper` 中,值为 `spring`,而对于 CDI,值为 `cdi`。 ## 5.1 修改 Mapper 添加下面代码到 `SimpleSourceDestinationMapper`: ```java @Mapper(componentModel = "spring") public interface SimpleSourceDestinationMapper ``` ## 5.2 将 Spring 组件注入到 Mapper 有时,我们需要在映射逻辑中使用其他 Spring 组件。在这种情况下,**我们必须使用抽象类而不是接口**: ```java @Mapper(componentModel = "spring") public abstract class SimpleDestinationMapperUsingInjectedService ``` 然后,我们可以使用众所周知的 `@Autowired` 注解轻松地注入所需的组件,并在代码中使用它: ```java @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. 映射不同名字的字段 在前面的示例中,MapStruct 能够自动映射 bean,因为它们具有相同的字段名。那么,如果我们要映射的 bean 具有不同的字段名呢? 在本例中,我们将创建一个名为 `Employee` 和 `EmployeeDTO` 的新 bean ## 6.1 新的 POJO ```java public class EmployeeDTO { private int employeeId; private String employeeName; // getters and setters } ``` ```java public class Employee { private int id; private String name; // getters and setters } ``` ## 6.2 Mapper 接口 当映射不同的字段名时,我们需要将其源字段配置为目标字段,为此,我们需要为每个字段添加 `@Mapping` 注解。 在 MapStruct 中,我们还可以使用点符号来定义 bean 的成员: ```java @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 测试用例 同样,我们需要测试源和目标对象值是否匹配: ```java @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()); } ``` 更多测试用例可以在 GitHub 项目中找到。 # 7. 映射具有子 Bean 的 Bean 接下来,我们将展示如何将一个 bean 映射到其他 bean 的引用。 ## 7.1 修改 POJO 让我们给 `Employee` 对象添加一个新的 bean 引用: ```java public class EmployeeDTO { private int employeeId; private String employeeName; private DivisionDTO division; // getters and setters omitted } ``` ```java public class Employee { private int id; private String name; private Division division; // getters and setters omitted } ``` ```java public class Division { private int id; private String name; // default constructor, getters and setters omitted } ``` ## 7.2 修改 Mapper 这里我们需要添加一种方法,将 `Division` 转换为 `DivisionDTO`,反之亦然;如果 MapStruct 检测到需要转换对象类型,并且要转换的方法存在于同一类中,它将自动使用它。 让我们将其添加到映射器中: ```java DivisionDTO divisionToDivisionDTO(Division entity); Division divisionDTOtoDivision(DivisionDTO dto); ``` ## 7.3 修改测试用例 让我们修改并添加一些测试用例到之前已存在的测试: ```java @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. 具有类型转换的映射 MapStruct 还提供了一些现成的隐式类型转换,对于我们的示例,我们将尝试将 String 日期转换为实际的 `Date` 对象。 有关隐式类型转换的更多详细信息,请查看 [MapStruct 参考指南](https://mapstruct.org/documentation/stable/reference/html/)。 ## 8.1 修改 Bean 我们为我们的职员添加一个开始日期: ```java public class Employee { // other fields private Date startDt; // getters and setters } ``` ```java public class EmployeeDTO { // other fields private String employeeStartDt; // getters and setters } ``` ## 8.2 修改 Mapper 我们修改映射器并提供 `dataFormat` 给我们的开始日期。 ```java @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 修改测试用例 让我们添加更多一些测试用例来验证转换是否正确: ```java 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` 功能的方式自定义映射器。 例如,除了类型转换之外,我们可能还希望以某种方式转换值,如下面的示例所示。 在这种情况下,我们可以创建一个抽象类并实现我们想要定制的方法,并将那些应该由 MapStruct 生成的方法保留为抽象类。 ## 9.1 基本模型 在这个例子中,我们将使用如下类: ```java public class Transaction { private Long id; private String uuid = UUID.randomUUID().toString(); private BigDecimal total; //standard getters } ``` 和一个对应的 DTO: ```java public class TransactionDTO { private String uuid; private Long totalInCents; // standard getters and setters } ``` 这里的棘手部分是将 `BigDecimal` `total` 的美元总额转换为 `Long` `totalInCents`。 ## 9.2 定义 Mapper 我们可以通过将我们的 Mapper 创建为抽象类来实现这个目标: ```java @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 toTransactionDTO( Collection transactions); } ``` 在这里,我们已经为单个对象转换实现了完全定制的映射方法。 另一方面,我们留下了方法,这意味着将 `Collection` 映射到 `List` 抽象,因此 MapStruct 将为我们实现它。 ## 9.3 生成的结果 由于我们已经实现了将单个 `Transaction` 映射到 `TransactionDTO` 的方法,因此我们希望 MapStruct 在第二个方法中使用它。 将生成以下内容: ```java @Generated class TransactionMapperImpl extends TransactionMapper { @Override public List toTransactionDTO(Collection transactions) { if ( transactions == null ) { return null; } List list = new ArrayList<>(); for ( Transaction transaction : transactions ) { list.add( toTransactionDTO( transaction ) ); } return list; } } ``` 我们可以在第 12 行中看到,MapStruct 在生成的方法中使用了我们实现。 # 10. Mapping 前后注解 这里有另一种使用 `@BeforeMapping` 和 `@AfterMapping` 注解自定义 `@Mapping` 功能的方法。**该注解用于标记映射逻辑前后调用的方法**。 它们在我们可能希望将此**行为应用于所有映射的超类型**的场景中非常有用。 让我们看一个将 `Car` `ElectricCar` 和 `BioDieselCar` 的子类型映射到 `CarDTO` 的示例。 在映射时,我们希望将类型的概念映射到 DTO 中的 `FuelType` 枚举字段。映射完成后,我们希望将 DTO 的名称更改为大写。 ## 10.1 基本模型 我们将使用下面类: ```java public class Car { private int id; private String name; } ``` `Car` 的子类: ```java public class BioDieselCar extends Car { } ``` ```java public class ElectricCar extends Car { } ``` 具有枚举类型 `FuelType` 的 `CarDTO`: ```java public class CarDTO { private int id; private String name; private FuelType fuelType; } ``` ```java public enum FuelType { ELECTRIC, BIO_DIESEL } ``` ## 10.2 定义 Mapper 让我们继续编写我们的抽象 mapper 类来映射 `Car` 到 `CarDTO`: ```java @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。 ## 10.3 结果 **上面的 `CarsMapper` 定义生成了实现:** ```java @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. Lombok 支持 在 MapStruct 的最新版本中,宣布了对 Lombok 的支持。**因此,我们可以使用 Lombok 轻松地映射源实体和目标。** 要启用 Lombok 支持,我们需要在注解处理器路径中添加依赖项。从 Lombok 版本 1.18.16 开始,我们还必须添加对 `lombok-mapstruct-binding` 的依赖。现在我们在 Maven 编译器插件中有了 `mapstruct-processor` 和 Lombok: ```xml org.apache.maven.plugins maven-compiler-plugin 3.5.1 1.8 1.8 org.mapstruct mapstruct-processor 1.5.3.Final org.projectlombok lombok 1.18.4 org.projectlombok lombok-mapstruct-binding 0.2.0 ``` 让我们使用 Lombok 注解定义源实体: ```java @Getter @Setter public class Car { private int id; private String name; } ``` 和目标数据转换对象: ```java @Getter @Setter public class CarDTO { private int id; private String name; } ``` 用于此的映射器接口与前面的示例类似: ```java @Mapper public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); CarDTO carToCarDTO(Car car); } ``` # 12. defaultExpression 支持 从 1.3.0 版开始,如果源字段为空,我们可以使用 `@Mapping` 注解的 `defaultExpression` 属性来指定一个表达式,该表达式确定目标字段的值。这是对现有 `defaultValue` 属性功能的补充。 源实体: ```java public class Person { private int id; private String name; } ``` 目标数据传输对象: ```java public class PersonDTO { private int id; private String name; } ``` 如果源实体的 `id` 字段为 `null`,我们希望生成一个随机 `id` 并将其分配给目标,保持其他属性值不变: ```java @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); } ``` 让我们添加一个测试用例来验证表达式的执行: ```java @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. 结论 本文介绍了 MapStruct。我们已经介绍了 Mapping 库的大部分基础知识以及如何在应用程序中使用它。 这些示例和测试的实现可以在 GitHub 项目中找到。这是一个 Maven 项目,因此应该很容易导入并按原样运行。