一、什么是 MapStruct

MapStruct 核心概念

MapStruct是一个Java注解处理器,它的主要功能是自动生成类型安全、高性能且无依赖的bean映射代码。这个工具基于“约定优于配置”的原则,极大地简化了Java Bean类型之间的映射实现过程。图片

在多层架构的应用中,经常需要在不同的对象模型之间进行转换,例如在持久层的实体和传输层的DTO(Data Transfer Object,数据传输对象)之间。手动编写这种映射代码是一项繁琐且容易出错的任务。MapStruct通过自动化的方式解决了这个问题,它可以在编译时生成映射代码,从而保证了高性能、快速的开发反馈以及严格的错误检查。

具体来说,使用MapStruct时,开发者只需要定义一个接口,并在接口中定义转换方法。然后,MapStruct会自动生成实现这些方法的代码。这些生成的代码使用纯方法调用,因此速度快、类型安全且易于理解。

MapStruc主要特性

1、类型安全:MapStruct在编译时生成映射代码并进行类型检查,如果源对象和目标对象的属性不匹配,会在编译阶段就报错。

2、性能优秀:由于MapStruct在编译时就生成了映射代码,运行时无需通过反射进行属性拷贝,因此性能较高。

3、灵活性:MapStruct支持复杂的映射,如嵌套映射、集合映射、自定义转换规则等。

4、简洁性:MapStruct使用注解来定义映射规则,使得映射规则的定义更加直观和简洁。

5、无依赖:MapStruct不依赖于任何第三方库,可以很容易地集成到任何项目中。

6、集成Spring:MapStruct也可以与Spring框架集成,允许在映射器中注入Spring管理的bean。

使用MapStruct,开发者只需要定义一个接口,并在接口中声明源对象和目标对象之间的映射关系,MapStruct会在编译时自动生成映射实现类。这极大地提高了代码的可读性和可维护性,同时也避免了手动编写繁琐的转换代码。

二、MapStruct和BeanUtils区别

MapStruct和BeanUtils都是Java中常用的对象属性映射工具,但它们在使用方式和性能上有一些区别。

「1、使用方式:」BeanUtils:使用反射机制进行属性拷贝,使用简单,无需写额外的映射代码。 MapStruct:需要定义映射接口,在编译阶段生成映射实现类,使用注解来定义源对象和目标对象之间的映射关系。

「2、性能:」BeanUtils:由于使用了反射机制,性能较低。 MapStruct:在编译阶段就生成了映射代码,运行时无需通过反射进行属性拷贝,因此性能较高。

「3、灵活性和安全性:」BeanUtils:由于是动态映射,如果源对象和目标对象的属性不匹配,可能会在运行时出现错误。 MapStruct:在编译阶段就进行了类型检查,如果源对象和目标对象的属性不匹配,会在编译阶段就报错,提高了类型安全性。另外,也支持复杂的映射,如嵌套映射、集合映射等。

先看一个案例:

 

@Mapperpublic interface CarMapper {    @BeanMapping(resultType = CarDto.class, ignoreByDefault = true, mappingControl = MappingControl.FILTER)    @Mapping(target = "color", ignore = true)    @Mapping(source = "model", target = "modelName")    @Mapping(condition = "java(source.getAge() >= 18)", target = "isAdult")    CarDto map(Car car);}

在这个例子中,我们定义了一个名为CarMapper的映射器接口,并使用了@Mapper注解将它标记为MapStruct映射器。然后,我们在映射方法上使用了@BeanMapping注解,并提供了以下配置: resultType = CarDto.class:指定映射方法的返回类型为CarDto。 ignoreByDefault = true:在目标类型CarDto中忽略所有未映射的属性。 mappingControl = MappingControl.FILTER:如果存在未匹配的属性,过滤它们而不报告错误或警告。

接下来,我们使用了@Mapping注解,对特定属性进行了额外的配置: target = “color”, ignore = true:忽略源对象的color属性,在目标对象CarDto中不进行映射。 source = “model”, target = “modelName”:将源对象的model属性映射到目标对象的modelName属性。 condition = “java(source.getAge() >= 18)”:添加条件判断,只有当源对象的age属性大于等于18时,才进行映射,并将结果映射到目标对象的isAdult属性。

通过这些配置,我们能够灵活地控制映射方法的行为。可以根据需求指定返回类型、忽略属性、设置映射控制策略,并添加条件判断来决定是否执行映射操作.

三、MapStruct的使用方法

1. 添加依赖

在你的pom.xml或者build.gradle文件中添加MapStruct的依赖: Maven:

<dependency>    <groupId>org.mapstruct</groupId>    <artifactId>mapstruct</artifactId>    <version>1.5.0.Final</version></dependency>

Gradle:

 

implementation 'org.mapstruct:mapstruct:1.5.0.Beta1'

2. 定义Mapper接口

定义一个Mapper接口,这个接口将包含你想要转换的方法。例如,假设你有两个类,Person和PersonDto,你想要将Person对象转换为PersonDto对象,那么你可以这样定义你的Mapper:

 

@Mapperpublic interface PersonMapper {    PersonMapper INSTANCE = Mappers.getMapper( PersonMapper.class );
    @Mapping(source = "name", target = "fullName")    PersonDto personToPersonDto(Person person);}

3. 使用Mapper

当你已经定义了Mapper接口后,你就可以在你的代码中使用它了:

 

Person person = new Person("John", "Doe");PersonMapper mapper = PersonMapper.INSTANCE;PersonDto personDto = mapper.personToPersonDto(person);

4. 自定义映射

在某些情况下,你可能需要自定义字段映射。你可以通过在@Mapping注解中使用expression或qualifiedByName参数来实现这一点

expression: 这个参数允许你使用Java表达式来定义字段映射。这在源和目标字段之间需要一些特定逻辑时非常有用。例如:

 

@Mapperpublic interface OrderMapper {    @Mapping(target = "orderDate", expression = "java(new java.text.SimpleDateFormat(\"yyyy-MM-dd\").format(order.getCreationDate()))")    OrderDto orderToOrderDto(Order order);}

在这个例子中,orderToOrderDto方法将Order的creationDate字段(类型为Date)转换为OrderDto的orderDate字段(类型为String),并且使用了特定的日期格式。

qualifiedByName: 这个参数允许你引用一个具有@Named注解的方法作为自定义的映射逻辑。例如:

@Mapperpublic interface OrderMapper {    @Mapping(target = "customerName", source = "customer", qualifiedByName = "fullName")    OrderDto orderToOrderDto(Order order);
    @Named("fullName")    default String customerToString(Customer customer) {        return customer.getFirstName() + " " + customer.getLastName();    }}

在这个例子中,orderToOrderDto方法将Order的customer字段(类型为Customer)转换为OrderDto的customerName字段(类型为String),并且使用了customerToString方法来获取全名。

5. 映射方法级别的详细配置

从MapStruct 1.5开始,可以使用@BeanMapping注解在MapStruct中用于在映射方法级别提供更详细的配置。这个注解有许多参数可以使用,例如,你可以选择在更新时忽略null值 以下是一些常见的使用场景:

resultType: 这个参数允许你指定映射方法的返回类型。这在目标类型可以是多个实现类时非常有用。 如果目标类型有多个实现类,并且你希望在映射时使用特定的实现类。通过指定resultType,你可以确保生成的映射代码使用正确的目标类型

 

@BeanMapping(resultType = CarDto.class)CarDto map(Car car);

qualifiedBy和qualifiedByName: 这两个参数允许你引用一个具有@Qualifier或@Named注解的方法作为自定义的映射逻辑。

 

@BeanMapping(qualifiedByName = "fullName")PersonDto personToPersonDto(Person person);
@Named("fullName")default String customerToString(Customer customer) {    return customer.getFirstName() + " " + customer.getLastName();}

ignoreByDefault: 这个参数允许你忽略所有未明确映射的属性。然后,你可以使用@Mapping注解来明确需要映射的属性。

 

@BeanMapping(ignoreByDefault = true)@Mapping(target = "name", source = "fullName")PersonDto personToPersonDto(Person person);

nullValuePropertyMappingStrategy: 这个参数允许你指定当源属性为null时应如何处理目标属性。例如,你可以选择是否在源属性为null时调用目标的setter方法。

 

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)PersonDto personToPersonDto(Person person);

6. 集合映射

MapStruct也支持集合的映射,你可以很方便地将一个对象的集合转换为另一个对象的集合。

@Mapperpublic interface CarMapper {    List<CarDto> carsToCarDtos(List<Car> cars);}

在这个例子中,carsToCarDtos方法将List转换为List。

7. 枚举映射

MapStruct 的 @ValueMapping 注解是用来映射枚举值的。这个注解只能在 @Mapper 的接口或抽象类中使用。 下面是一个简单的例子,展示了如何使用 @ValueMapping 在两个枚举类型之间进行映射: 首先,我们定义两个枚举类型:

public enum SourceEnum {    TYPE_A,    TYPE_B,    TYPE_C}
public enum TargetEnum {    TYPE_X,    TYPE_Y,    TYPE_Z}

然后,我们创建一个映射器接口并使用 @ValueMapping:

@Mapperpublic interface EnumMapper {    @ValueMappings({        @ValueMapping(source = "TYPE_A", target = "TYPE_X"),        @ValueMapping(source = "TYPE_B", target = "TYPE_Y"),        @ValueMapping(source = "TYPE_C", target = "TYPE_Z")    })    TargetEnum sourceToTarget(SourceEnum sourceEnum);}

在上述代码中,我们定义了一个 sourceToTarget 方法,它将 SourceEnum 对象映射到 TargetEnum 对象。@ValueMapping 注解指定了源枚举值到目标枚举值的映射。

此外,MapStruct 还提供了特殊的源/目标值 NULL 和 ANY,可以用于处理源枚举值为 null 或未映射的情况。例如:

@Mapperpublic interface EnumMapper {    @ValueMappings({        @ValueMapping(source = "TYPE_A", target = "TYPE_X"),        @ValueMapping(source = "TYPE_B", target = "TYPE_Y"),        @ValueMapping(source = "TYPE_C", target = "TYPE_Z"),        @ValueMapping(source = "NULL", target = "TYPE_Z"),        @ValueMapping(source = "ANY", target = "TYPE_X")    })    TargetEnum sourceToTarget(SourceEnum sourceEnum);}

在这个例子中,如果源枚举值为 null,则目标枚举值为 TYPE_Z;如果源枚举值没有映射(即源枚举有其他值),则目标枚举值为 TYPE_X。

8. 使用构造函数映射

从MapStruct 1.5开始,你可以使用构造函数来创建目标对象。你只需要在你的目标类中定义一个合适的构造函数,MapStruct就会自动使用它。

public class CarDto {    private final String make;    private final int seatCount;
    public CarDto(String make, int seatCount) {        this.make = make;        this.seatCount = seatCount;    }    // getters}

然后在你的Mapper接口中定义映射方法:

@Mapperpublic interface CarMapper {    CarDto carToCarDto(Car car);}

MapStruct将使用CarDto的构造函数来创建新的CarDto实例。

9. 嵌套属性映射

MapStruct也支持嵌套属性的映射。例如,如果你的Car类有一个Engine属性,你可以这样定义你的Mapper:

@Mapperpublic interface CarMapper {    @Mapping(source = "engine.horsePower", target = "horsePower")    CarDto carToCarDto(Car car);}

在这个例子中,carToCarDto方法将Car的engine.horsePower属性映射到CarDto的horsePower属性。

10. 反向映射

MapStruct还提供了反向映射的功能。你可以使用@InheritInverseConfiguration注解来创建反向的映射方法:

@Mapperpublic interface CarMapper {    CarDto carToCarDto(Car car);
    @InheritInverseConfiguration    Car carDtoToCar(CarDto carDto);}

在这个例子中,carDtoToCar方法是carToCarDto方法的反向映射。

11. 使用装饰器增强Mapper

你可以使用装饰器来增强你的Mapper。首先,定义一个装饰器类:

public abstract class CarMapperDecorator implements CarMapper {    private final CarMapper delegate;    public CarMapperDecorator(CarMapper delegate) {        this.delegate = delegate;    }
    @Override    public CarDto carToCarDto(Car car) {        CarDto dto = delegate.carToCarDto(car);        dto.setMake(dto.getMake().toUpperCase());        return dto;    }}

然后在你的Mapper接口中使用@DecoratedWith注解:

 

@Mapper@DecoratedWith(CarMapperDecorator.class)public interface CarMapper {    CarDto carToCarDto(Car car);}

12. 映射继承

可以使用@InheritConfiguration注解使一个映射方法继承另一个映射方法的配置。例如:

@Mapperpublic interface MyMapper {
    @Mapping(target = "name", source = "name")    @Mapping(target = "description", source = "description")    Target sourceToTarget(Source source);
    @InheritConfiguration    Target updateTargetFromSource(Source source, @MappingTarget Target target);}

在上述代码中,我们首先定义了一个 sourceToTarget 方法,它将 Source 对象映射到 Target 对象。然后,我们定义了一个 updateTargetFromSource 方法,它接受一个 Source 对象和一个 Target 对象,并使用 @InheritConfiguration 注解来指示 MapStruct 使用 sourceToTarget 方法的映射配置。这意味着 updateTargetFromSource 方法将使用与 sourceToTarget 相同的映射规则。

注意:@InheritConfiguration 会寻找签名(参数数量和类型)最匹配的方法来继承配置,如果有多个匹配的方法,你需要使用 @InheritConfiguration(name=”…”) 来明确指定要继承的方法。

13. 使用@BeforeMapping和@AfterMapping进行预处理和后处理

你可以使用@BeforeMapping和@AfterMapping注解来进行映射前后的处理:

@Mapperpublic abstract class CarMapper {    @BeforeMapping    protected void enrichCar(Car car) {        car.setMake(car.getMake().toUpperCase());    }
    @Mapping(source = "numberOfSeats", target = "seatCount")    public abstract CarDto carToCarDto(Car car);
    @AfterMapping    protected void enrichDto(Car car, @MappingTarget CarDto carDto) {        carDto.setSeatCount(car.getNumberOfSeats());    }}

在这个例子中,enrichCar方法在映射前被调用,enrichDto方法在映射后被调用。

14. 使用@Context传递上下文参数

你可以使用@Context注解来传递上下文参数给映射方法:

@Mapperpublic interface OrderMapper {    @Mapping(target = "customer", source = "entity.customer", qualifiedByName = "fullName")    OrderDto orderToOrderDto(Order entity, @Context CycleAvoidingMappingContext context);
    @Named("fullName")    default String customerToString(Customer customer) {        return customer.getFirstName() + " " + customer.getLastName();    }}

在这个例子中,context参数被用于避免循环引用。

15. 映射更新

MapStruct允许你将一个对象的属性更新到另一个已存在的对象。例如:

@Mapperpublic interface CarMapper {    @Mapping(source = "numberOfSeats", target = "seatCount")    void updateCarFromDto(CarDto carDto, @MappingTarget Car car);}

在这个例子中,updateCarFromDto方法将CarDto的属性更新到已存在的Car对象。

17. 忽略某些字段

有时候,你可能想要忽略源对象中的某些字段。你可以使用@Mapping注解的ignore参数来实现这一点:

@Mapperpublic interface CarMapper {    @Mapping(target = "seatCount", ignore = true)    CarDto carToCarDto(Car car);}

在这个例子中,carToCarDto方法将忽略Car的seatCount字段。

18. 常量映射

@Mapping注解constant属性可以用于将源对象的某个固定值映射到目标对象的属性:

@Mapperpublic interface CarMapper {    @Mapping(target = "carType", constant = "SEDAN")    CarDto carToCarDto(Car car);}

在这个例子中,carToCarDto方法将会把CarDto的carType字段设置为”SEDAN”,无论Car对象的实际内容如何。

19. 使用@Mappings定义多个映射规则

你可以使用@Mappings注解来定义多个映射规则:

@Mapperpublic interface CarMapper {    @Mappings({        @Mapping(source = "numberOfSeats", target = "seatCount"),        @Mapping(source = "manufacturingDate", target = "year")    })    CarDto carToCarDto(Car car);}

在这个例子中,carToCarDto方法将Car的numberOfSeats属性映射到CarDto的seatCount属性,将manufacturingDate属性映射到year属性。

20. 使用Builder模式

如果你的目标对象使用了Builder模式,MapStruct也能很好地支持。你只需要在@Mapper注解中指定builder的类名:

@Mapper(builder = @Builder(builderClassName = "Builder", buildMethodName = "build"))public interface CarMapper {    CarDto carToCarDto(Car car);}

21. 默认值映射

MapStruct也支持默认值映射,你可以使用@Mapping注解的defaultValue参数来实现这一点:

@Mapperpublic interface CarMapper {    @Mapping(target = "seatCount", source = "numberOfSeats", defaultValue = "4")    CarDto carToCarDto(Car car);}

在这个例子中,如果Car的numberOfSeats属性为null,那么carToCarDto方法会将CarDto的seatCount字段设置为”4″。

22. 使用@MapperConfig配置全局映射策略

@MapperConfig 注解在 MapStruct 中用于定义全局或共享的映射配置。这个注解可以被应用在接口或抽象类上,然后其他的 @Mapper 可以通过 config 属性引用这个配置。 下面是一个简单的例子: 首先,我们定义一个全局的映射配置:

@MapperConfig(componentModel = "spring", uses = DateMapper.class)public interface GlobalMapperConfig {}

在这个例子中,我们指定了 componentModel 为 “spring”,这意味着生成的映射器将是 Spring 的组件,可以使用 @Autowired 进行注入。我们还指定了 uses 属性为 DateMapper.class,这意味着所有引用这个配置的映射器都可以使用 DateMapper 中定义的方法进行映射。 然后,我们创建一个映射器并引用这个全局配置:

@Mapper(config = GlobalMapperConfig.class)public interface MyMapper {    // mapping methods}

在这个例子中,MyMapper 将继承 GlobalMapperConfig 中定义的所有配置。 注意,如果 @Mapper 和 @MapperConfig 中都定义了相同的属性,那么 @Mapper 中的属性将会覆盖 @MapperConfig 中的属性。

@MapperConfig 是一种强大的工具,可以帮助你减少重复的配置,并使你的代码更易于维护

23. 使用@IterableMapping和@MapMapping处理集合

当处理集合和映射时,你可能需要特定的转换规则。@IterableMapping 注解的作用是定义一个方法,用于将一个 Iterable 类型的源对象集合映射为目标对象集合。

具体来说,@IterableMapping 注解用于标记一个接口方法,该方法的参数类型为源对象集合,返回类型为目标对象集合。在生成的映射代码中,MapStruct 会将每个源对象映射为一个目标对象,并将它们添加到目标对象集合中。需要注意的是,源对象集合和目标对象集合的元素类型可以不同,此时需要手动指定元素类型转换方式。

@IterableMapping 注解还有一些属性,用于配置映射的行为,例如: qualifiedBy:用于指定一个限定符注解,当存在多个映射器时,可以使用该属性来选择特定的映射器。 elementTargetType:用于指定目标对象集合的元素类型。 nullValueMappingStrategy:用于处理源对象集合中包含空对象或者 null 值的情况。 一个示例的 @IterableMapping 注解的使用方式如下所示:

@Mapperpublic interface UserMapper {    @IterableMapping(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT)    List<UserDTO> toUserDTOList(Iterable<User> users);}

上述代码中,UserMapper 接口中的 toUserDTOList 方法使用了 @IterableMapping 注解,用于将 User 集合转换为 UserDTO 集合。其中,nullValueMappingStrategy 属性指定当源对象集合中包含空对象或者 null 值时,返回默认值。

24. 使用@MapMapping 注解来处理 Map 类型的映射

@MapMapping 注解用于方法级别,指示 MapStruct 如何映射 Map 类型的属性。你可以在映射器接口中的方法上使用该注解,并提供一些配置选项。

@Mapperpublic interface CarMapper {    @MapMapping(keyTargetType = String.class, valueTargetType = CarDto.class)    Map<String, CarDto> carsToCarDtos(Map<String, Car> cars);}

在这个示例中,我们定义了一个名为 CarMapper 的映射器接口,并使用了 @Mapper 注解将它标记为 MapStruct 映射器。

然后,我们在 carsToCarDtos 方法上使用了 @MapMapping 注解,并提供了以下配置选项:

keyTargetType = String.class:指定目标键类型为 String。这会告诉 MapStruct 将源 Map 的键映射为 String 类型。 valueTargetType = CarDto.class:指定目标值类型为 CarDto。这会告诉 MapStruct 将源 Map 的值映射为 CarDto 类型。 通过这样配置 @MapMapping 注解,MapStruct 将自动生成适当的映射代码,按照指定的映射规则将源 Map 中的键值对映射到目标 Map。

需要注意的是,如果你的映射逻辑更加复杂,可以在 @MapMapping 注解的方法参数中提供自定义的转换器。例如:

@Mapperpublic interface CarMapper {    @MapMapping(keyTargetType = String.class, valueTargetType = CarDto.class,                keyQualifiedBy = {ToUpperCase.class}, valueQualifiedBy = {ConvertValue.class})    Map<String, CarDto> carsToCarDtos(Map<String, Car> cars);
    @Qualifier    @Target(ElementType.METHOD)    @Retention(RetentionPolicy.CLASS)    public @interface ToUpperCase {    }
    @Qualifier    @Target(ElementType.METHOD)    @Retention(RetentionPolicy.CLASS)    public @interface ConvertValue {        Class<?> targetType();    }
    @ToUpperCase    public String toUpperCase(String key) {        return key.toUpperCase();    }
    @ConvertValue(targetType = CarDto.class)    public CarDto convertValue(Car car) {        // Custom conversion logic        return new CarDto(car.getMake(), car.getNumberOfSeats());    }}

在这个示例中,我们添加了自定义的转换器。通过使用 keyQualifiedBy 和 valueQualifiedBy 参数,我们可以指定用于键和值的转换器。

我们定义了两个自定义的限定符注解 @ToUpperCase 和 @ConvertValue,并在转换器方法上使用它们。然后,在 carsToCarDtos 方法上分别指定了这两个限定符注解。

这样,当 MapStruct 遇到需要转换键或值的情况时,它将使用相应的转换器方法来进行转换。

24. 使用@Qualifier自定义映射方法选择

@Qualifier 注解用于标识自定义转换器方法和映射过程中的限定符。 通过使用 @Qualifier 注解,你可以为转换器方法或映射方法提供更具体的选择标准,以决定哪个方法应该被调用。

下面是一个示例,展示了如何在 MapStruct 1.5 中使用 @Qualifier 注解:

@Mapperpublic interface CarMapper {    @Qualifier    @Target(ElementType.METHOD)    @Retention(RetentionPolicy.CLASS)    public @interface FastCar {}
    @Qualifier    @Target(ElementType.METHOD)    @Retention(RetentionPolicy.CLASS)    public @interface ElectricCar {}
    // 转换器方法1    @FastCar    CarDto convertToFastCarDto(Car car);
    // 转换器方法2    @ElectricCar    CarDto convertToElectricCarDto(Car car);
    // 映射方法    @Mapping(target = "carDto", qualifiedBy = {FastCar.class, ElectricCar.class})    GarageDto mapGarageToDto(Garage garage);}

在这个示例中,我们定义了两个注解 @FastCar 和 @ElectricCar,它们都是通过 @Qualifier 注解来定义的。这些注解用于标识转换器方法 convertToFastCarDto 和 convertToElectricCarDto。

然后,在映射方法 mapGarageToDto 上,我们使用了 qualifiedBy 参数来标记多个限定符。通过这样配置,MapStruct 将根据指定的限定符选择适当的转换器方法来进行映射。

请注意,@Qualifier 注解需要与自定义转换器方法一起使用。你可以根据实际需求定义自己的限定符注解,并将其应用于合适的转换器方法上。

扫码领红包

微信赞赏支付宝扫码领红包

发表回复

后才能评论