欢迎光临散文网 会员登陆 & 注册

一文读懂MapStruct 1.5.5.Final

2023-06-30 11:43 作者:啥玩意你再说一遍  | 我要投稿

前言

我们在平时项目开发过程中会经常这样的情况,从数据库中查出来的PO对象中包含很多字段,但是在业务处理过程中需要处理的DTO包含PO中的部分字段,或者返回给前端的VO为DTO的部分字段,这就需要Class类转化,如果用构造器或者get()/set()方法,将会写大量冗余代码并且容易出错。面对这样的场景,采用MapStruct插件处理类转换是一个绝佳的选择。

MapStruct 是一个注释处理器,用于生成类型安全、高性能和无依赖的 Bean 映射代码。

MapStruct与Project Lombok一起工作,MapStruct利用生成的getter、setter和构造函数,并使用它们来生成映射器实现。

1简介

MapStruct是一个Java注释处理器,用于生成类型安全的bean映射类。

您所要做的就是定义一个映射器接口,该接口声明任何所需的映射方法。在编译期间,MapStruct将生成这个接口的实现。这个实现使用普通的Java方法调用在源对象和目标对象之间进行映射。

2设置

2.1Maven配置

...
<properties>
   <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>
...
<dependencies>
   <dependency>
       <groupId>org.mapstruct</groupId>
       <artifactId>mapstruct</artifactId>
       <version>${org.mapstruct.version}</version>
   </dependency>
</dependencies>
...
<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.8.1</version>
           <configuration>
               <source>1.8</source>
               <target>1.8</target>
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>${org.mapstruct.version}</version>
                   </path>
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>
...

这个 pom.xml 配置部分是 Maven 构建工具的配置,用于指定项目的编译插件和相关配置。

在这个配置中,我们使用了 maven-compiler-plugin 插件,并指定了插件的具体配置。下面是这个配置的解释:

  • <groupId>org.apache.maven.plugins</groupId>:指定了插件的 GroupId,用于唯一标识插件的组织或项目。

  • <artifactId>maven-compiler-plugin</artifactId>:指定了插件的 ArtifactId,用于唯一标识插件的名称。

  • <version>3.8.1</version>:指定了插件的版本号,用于确定使用的插件版本。

  • <configuration>:指定了插件的配置信息,包含了源代码版本、目标代码版本和注解处理器路径等配置项。

  • <source>1.8</source>:指定了项目的源代码版本为 Java 1.8。

  • <target>1.8</target>:指定了项目的目标代码版本为 Java 1.8。

  • <annotationProcessorPaths>:指定了注解处理器的路径。在这个配置中,我们添加了 org.mapstruct:mapstruct-processor 的注解处理器,${org.mapstruct.version} 是一个变量,表示从项目的属性或外部配置文件中获取的注解处理器版本。

总结来说,这个配置的目的是告诉 Maven 在构建项目时使用 maven-compiler-plugin 插件进行编译,并指定了源代码和目标代码的版本为 Java 1.8。同时,它还配置了 org.mapstruct:mapstruct-processor 的注解处理器,用于处理项目中使用 MapStruct 的注解。这样,当编译项目时,MapStruct 注解会被正确处理,并生成对应的映射代码。

注解处理器通常是作为插件的一部分进行配置,因此常常会在 <configuration> 中指定注解处理器的路径。这是因为注解处理器是在编译阶段执行的,并且常常需要特定的插件来识别和处理这些注解。通过在插件的配置中定义注解处理器路径,可以确保在相应的编译阶段执行注解处理器。

然而,在某些情况下,注解处理器可能也作为依赖项进行引入。它们可以像其他库一样在 <dependencies> 中声明,并在项目构建过程中使用。这种方法可能适用于一些通用的注解处理器,例如 Lombok、MapStruct 等。

如果要将注解处理器作为依赖项进行引入,可以在 <dependencies> 下添加相应的依赖项。例如:

<dependencies>
   <!-- 其他依赖项 -->
   <dependency>
       <groupId>org.mapstruct</groupId>
       <artifactId>mapstruct-processor</artifactId>
       <version>${org.mapstruct.version}</version>
       <scope>provided</scope>
   </dependency>
</dependencies>

在这个例子中,我们将 MapStruct 的注解处理器作为依赖项引入,并且将其范围设置为 provided。这意味着该依赖项在编译和测试时可用,但在运行时将由目标环境提供。这种引入注解处理器的方式是为了解决在没有特定插件的情况下,以依赖项的方式使用注解处理器。然而,对于需要特定插件支持的注解处理器,建议仍然将其配置在插件的 <configuration> 中。

2.2配置选项

MapStruct代码生成器可以使用注释处理器选项进行配置。

当直接调用javac时,这些选项以- key=value的形式传递给编译器。当通过Maven使用MapStruct时,任何处理器选项都可以在Maven处理器插件的配置中使用compilerArgs传递,如下所示:

...
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-compiler-plugin</artifactId>
   <version>3.5.1</version>
   <configuration>
       <source>1.8</source>
       <target>1.8</target>
       <annotationProcessorPaths>
           <path>
               <groupId>org.mapstruct</groupId>
               <artifactId>mapstruct-processor</artifactId>
               <version>${org.mapstruct.version}</version>
           </path>
       </annotationProcessorPaths>
       <!-- due to problem in maven-compiler-plugin, for verbose mode add showWarnings -->
       <showWarnings>true</showWarnings>
       <compilerArgs>
           <arg>
               -Amapstruct.suppressGeneratorTimestamp=true
           </arg>
           <arg>
               -Amapstruct.suppressGeneratorVersionInfoComment=true
           </arg>
           <arg>
               -Amapstruct.verbose=true
           </arg>
       </compilerArgs>
   </configuration>
</plugin>
...

OptionPurposeDefaultmapstruct. suppressGeneratorTimestamp如果设置为' true ',将禁止在生成的映射器类中的' @Generated '注释中创建时间戳。falsemapstruct.verbose如果设置为' true ', MapStruct记录其主要决策的MapStruct。注意,在用Maven编写代码时,由于Maven -compiler-plugin配置中的一个问题,还需要添加' showWarnings '。falsemapstruct. suppressGeneratorVersionInfoComment如果设置为“true”,生成的映射器类中的“@Generated”注释中的“comment”属性的创建将被禁止。注释包含有关MapStruct版本和用于注释处理的编译器的信息。falsemapstruct.defaultComponentModel组件模型的名称(请参阅检索映射器),应该根据该名称生成映射器。 支持的值有: default:映射器不使用组件模型,实例通常通过Mappers#getMapper(Class)来检索。 cdi:生成的映射器是应用程序作用域的CDI bean,可以通过@Inject进行检索(from javax.enterprise.context or jakarta.enterprise.context, depending on which one is available with javax.inject having priority)。 spring: 生成的映射器是一个单例作用域的Spring bean,可以通过@Autowired进行检索。 jsr330:生成的映射器用{@code @Named}注释,可以通过@Inject(from javax.inject or jakarta.inject, depending which one is available with javax.inject having priority)来检索,例如使用Spring。 jakarta:生成的映射器用{@code @Named}注释,可以通过@Inject (from jakarta.inject)来检索,例如使用Spring。 jakarta-cdi: 生成的映射器是应用程序范围(来自jakarta.enterprise.context)的CDI bean,可以通过@Inject进行检索。 如果通过@Mapper#componentModel()为特定的映射器提供组件模型,则来自注释的值优先。defaultmapstruct.defaultInjectionStrategy通过参数使用在映射器中注入的类型。这只用于基于注释的组件模型,如CDI、Spring和JSR 330。 支持的值有: field: 依赖项将被注入到字段中。 constructor: 将生成构造函数。依赖项将通过构造函数注入。 当CDI componentModel时,也会生成一个默认构造函数。如果通过@Mapper#injectionStrategy()为特定的映射器提供了注入策略,则注释中的值优先于该选项。fieldmapstruct.unmappedTargetPolicy如果映射方法的目标对象的属性没有使用源值填充,则应用默认报告策略。 支持的值有: ERROR:任何未映射的目标属性将导致映射代码生成失败  WARN:任何未映射的目标属性将在构建时引起警告  IGNORE:忽略未映射的目标属性 如果通过@Mapper#unmappedTargetPolicy()为特定的映射器提供策略,则注释中的值优先。如果通过@BeanMapping#unmappedTargetPolicy()为特定的bean映射提供策略,则该策略优先于@Mapper#unmappedTargetPolicy()和选项。WARNmapstruct.unmappedSourcePolicy如果映射方法的源对象的属性没有使用目标值填充,则应用默认报告策略。 支持的值有: ERROR:任何未映射的源属性将导致映射代码生成失败  WARN:任何未映射的源属性将在构建时引起警告  IGNORE:忽略未映射的源属性 如果通过@Mapper#unmappedSourcePolicy()为特定的映射器提供策略,则注释中的值优先。如果通过@BeanMapping#ignoreUnmappedSourceProperties()为特定的bean映射提供策略,则该策略优先于@Mapper#unmappedSourcePolicy()和选项。WARNmapstruct. disableBuilders如果设置为true,那么MapStruct在进行映射时将不使用构建器模式。这相当于对所有的映射器执行@Mapper(builder = @Builder(disableBuilder = true))。false

2.3IDEA MapStruct插件

IDEA下集成MapStruct需要安装插件。

MapStruct 插件(MapStruct Support)可以提供以下功能:

  1. 自动生成映射类:MapStruct 插件可以自动生成用于对象之间的映射的实现类。这样,你可以通过指定映射规则和注解来生成映射代码,而无需手动编写大量的映射代码。

  2. 显示映射错误和警告:MapStruct 插件可以在编辑器中显示映射错误和警告。这有助于提前发现潜在的问题,并在编译之前解决它们。

  3. 提供快速修复和重构功能:MapStruct 插件可以提供一些有用的快速修复和重构功能,例如自动添加映射方法、自动修复映射错误等。这可以帮助提高开发效率并减少手动操作。

3定义映射器

3.1映射

要创建一个映射器,只需定义一个带有所需映射方法的Java接口,并用org.mapstruct.Mapper注释对其进行注释:

@Mapper
public interface CarMapper {

   @Mapping(target = "manufacturer", source = "make")
   @Mapping(target = "seatCount", source = "numberOfSeats")
   CarDto carToCarDto(Car car);

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

@Mapper注释导致MapStruct代码生成器在构建时创建CarMapper接口的实现。

在生成的方法实现中,源类型(例如Car)的所有可读属性将被复制到目标类型(例如CarDto)的相应属性中:

  • 当属性与其对应的目标实体具有相同的名称时,它将被隐式映射。(也就是不需要写@Mapping(target = "manufacturer", source = "make")这样的映射关系代码)

  • 当属性在目标实体中具有不同的名称时,可以通过@Mapping注释指定其名称。

JavaBeans规范中定义的属性名称必须在@Mapping注释中指定,例如,使用accessor方法getSeatCount()和setSeatCount()来指定属性的seatCount。

通过@BeanMapping(ignoreByDefault = true),默认行为将是显式映射,这意味着所有映射都必须通过@Mapping来指定,并且不会对丢失的目标属性发出警告。这允许忽略所有字段,除了那些通过@Mapping显式定义的字段。

@BeanMapping 用于指定在对象映射过程中自定义映射规则的注解。它可以用于方法级别或类级别。

在方法级别,@BeanMapping 注解应用于映射方法上,用于指定源对象和目标对象之间的映射规则。它可以包含以下属性:

  • ignore:指定是否忽略映射规则,默认为 false。当设置为 true 时,表示忽略该映射规则,不生成相应的映射代码。

  • nullValuePropertyMappingStrategy: 指定如何映射源对象的 null 值,默认为 NullValuePropertyMappingStrategy.SET_TO_NULL。可以设置为以下值:

    • NullValuePropertyMappingStrategy.SET_TO_NULL:将目标属性设置为 null。

    • NullValuePropertyMappingStrategy.STRICT:如果源属性为 null,则引发异常。

    • NullValuePropertyMappingStrategy.IGNORE:忽略源属性为 null 的情况。

  • nullValueCheckStrategy: 指定如何进行源对象的 null 值检查,默认为 NullValueCheckStrategy.ALWAYS。可以设置为以下值:

    • NullValueCheckStrategy.ALWAYS:始终检查源对象是否为 null。

    • NullValueCheckStrategy.ON_IMPLICIT_CONVERSION:仅在存在隐式转换的情况下才检查源对象是否为 null。

    • NullValueCheckStrategy.ON_IMPLICIT_CONVERSION_AND_EXPLICIT:在存在隐式转换或显式转换的情况下才检查源对象是否为 null。

    • NullValueCheckStrategy.NEVER:永不检查源对象是否为 null。

在类级别,@BeanMapping 注解应用于映射接口上,用于指定默认的映射规则。类级别的 @BeanMapping 注解可以包含与方法级别注解相同的属性,用于指定默认的映射规则,这些规则将适用于该映射接口中的所有映射方法。

  • Spring框架中使用映射

要在 Spring 中使用 MapStruct 的映射,你可以按照以下步骤引入映射到 Spring 的 @Service 中:

  1. 首先,确保你已经在项目的依赖中添加了 MapStruct 的相关依赖。这通常包括 mapstructmapstruct-processor

  2. 创建一个 @Mapper 注解的接口,例如 CarMapper,并在方法上使用 MapStruct 的相关注解,如 @Mapping

  3. CarMapper 接口的实现类中添加 @Mapper(componentModel = "spring") 注解。这将告诉 MapStruct 在生成实现类时使用 Spring 的组件模型,以便将生成的实现类作为 Spring 的组件进行管理。

@Mapper(componentModel = "spring")
public interface CarMapper {
   // 映射方法
}

4.在 @Service 类中引入 CarMapper 作为依赖,并使用 @Autowired@Resource 进行注入。

@Service
public class CarService {
   private final CarMapper carMapper;

   @Autowired
   public CarService(CarMapper carMapper) {
       this.carMapper = carMapper;
   }
   // 使用 CarMapper 进行映射
}

现在,你可以在 CarService 中使用 carMapper 进行对象的映射操作。Spring 将会自动注入 carMapper 的实例,并通过 MapStruct 提供的映射规则将源对象映射到目标对象。这样,你就可以在 @Service 类中方便地使用 MapStruct 进行对象之间的转换。

  • 非Spring框架中使用映射

    创建一个工厂类,在该类中获取 CarMapper 的实例。示例如下:

    public class CarMapperFactory {

       private static final CarMapper MAPPER = Mappers.getMapper(CarMapper.class);

       public static CarMapper getCarMapper() {
           return MAPPER;
       }
    }

    在代码中,通过工厂类获取 CarMapper 实例,并使用它进行对象之间的映射。

    CarMapper carMapper = CarMapperFactory.getCarMapper();
    CarDto carDto = carMapper.carToCarDto(car);

或者直接在映射接口中添加属性

CarMapper MAPPER = Mappers.getMapper(CarMapper.class);

3.2映射实现

为了更好地理解MapStruct做了什么,看看下面由MapStruct生成的carToCarDto()方法的实现:

// GENERATED CODE
public class CarMapperImpl implements CarMapper {

   @Override
   public CarDto carToCarDto(Car car) {
       if ( car == null ) {
           return null;
       }

       CarDto carDto = new CarDto();

       if ( car.getFeatures() != null ) {
           carDto.setFeatures( new ArrayList<String>( car.getFeatures() ) );
       }
       carDto.setManufacturer( car.getMake() );
       carDto.setSeatCount( car.getNumberOfSeats() );
       carDto.setDriver( personToPersonDto( car.getDriver() ) );
       carDto.setPrice( String.valueOf( car.getPrice() ) );
       if ( car.getCategory() != null ) {
           carDto.setCategory( car.getCategory().toString() );
       }
       carDto.setEngine( engineToEngineDto( car.getEngine() ) );

       return carDto;
   }

   @Override
   public PersonDto personToPersonDto(Person person) {
       //...
   }

   private EngineDto engineToEngineDto(Engine engine) {
       if ( engine == null ) {
           return null;
       }

       EngineDto engineDto = new EngineDto();

       engineDto.setHorsePower(engine.getHorsePower());
       engineDto.setFuel(engine.getFuel());

       return engineDto;
   }
}

MapStruct的一般理念是生成看起来尽可能像您自己手写的代码,通过简单的getter/setter调用而不是反射或类似的方法将值从源复制到目标。

观察上述代码

carDto.setDriver( personToPersonDto( car.getDriver() ) );

也就是说MapStruct实现类属性映射,是根据源类与目标类的属性名称来匹配的。在CarDTO.class中存在private PersonDto driver;成员属性,在Car.class中存在private Person driver;成员属性。

如示例所示,生成的代码会考虑通过@Mapping指定的任何名称映射。如果映射属性的类型在源实体和目标实体中不同(例如price属性,请参见隐式类型转换),MapStruct将应用自动转换或可选地调用/创建另一个映射方法(例如,对于driver / engine属性)。只有当且仅当源和目标属性是Bean的属性并且它们本身是Bean或简单属性时,MapStruct才会创建一个新的映射方法。

3.3映射器添加自定义方法

在某些情况下,可能需要手动实现从一种类型到另一种类型的特定映射,而这种映射不能由MapStruct生成。处理这个问题的一种方法是在另一个类上实现自定义方法,然后由MapStruct生成的映射器使用。

另外,当使用Java 8或更高版本时,您可以直接在映射器接口中实现自定义方法作为默认方法。如果参数和返回类型匹配,生成的代码将调用默认方法。

作为一个例子,让我们假设从Person到PersonDto的映射需要一些特殊的逻辑,这些逻辑不能由MapStruct生成。然后你可以像这样从前面的例子中定义映射器,这样Person.class转PersonDto.class将按照自定义逻辑转换。

@Mapper
public interface CarMapper {

   @Mapping(...)
   ...
   CarDto carToCarDto(Car car);

   default PersonDto personToPersonDto(Person person) {
       //hand-written mapping logic
   }
}

由MapStruct生成的类实现了方法carToCarDto()。当映射驱动属性时,在carToCarDto()中生成的代码将调用手动实现的personToPersonDto()方法。

MapStruct将生成CarMapper的一个子类,并实现carcardto()方法,因为它被声明为抽象的。当映射驱动属性时,在carToCarDto()中生成的代码将调用手动实现的personToPersonDto()方法。

3.4具有多个源参数的映射方法

MapStruct还支持带有多个源参数的映射方法。例如,为了将几个实体组合成一个数据传输对象。示例如下:

@Mapper
public interface AddressMapper {

   @Mapping(target = "description", source = "person.description")
   @Mapping(target = "houseNumber", source = "address.houseNo")
   DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}

所示的映射方法接受两个源参数,并返回一个组合的目标对象。与单参数映射方法一样,属性是按名称映射的。

如果多个源对象定义具有相同名称的属性,则必须使用@Mapping注释指定从中检索属性的源参数,如示例中的description属性所示。当这种歧义未解决时将引发错误。对于在给定源对象中只存在一次的属性,指定源参数的名称是可选的,因为它可以自动确定。

在使用@Mapping注释时,必须指定属性所在的参数。

如果所有源参数都为空,具有多个源参数的映射方法将返回空。否则,将实例化目标对象,并传播来自所提供参数的所有属性。

MapStruct还提供了直接引用源参数的可能性。

@Mapper
public interface AddressMapper {

   @Mapping(target = "description", source = "person.description")
   @Mapping(target = "houseNumber", source = "hn")
   DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}

在本例中,source参数直接映射到目标,如上面的示例所示。参数hn,一个非bean类型(在本例中为java.lang.Integer)被映射到houseNumber。

3.5将嵌套bean属性映射到当前目标

如果不想显式地命名嵌套源bean中的所有属性,可以使用.作为目标。这将告诉MapStruct将每个属性从源bean映射到目标对象。示例如下:

@Mapper
public interface CustomerMapper {

    @Mapping( target = "name", source = "record.name" )
    @Mapping( target = ".", source = "record" )
    @Mapping( target = ".", source = "account" )
    Customer customerDtoToCustomer(CustomerDto customerDto);
}

生成的代码将把每个属性从CustomerDto.record直接映射到Customer,而不需要手动命名它们中的任何一个。对于Customer.account也是如此。

当存在冲突时,可以通过显式定义映射来解决这些冲突。例如在上面的例子中。name出现在CustomerDto中。record在CustomerDto.account中。映射@Mapping(target = "name", source = "record.name")解决了这个冲突。

这种“target this”符号在将分层对象映射到平面对象时非常有用,反之亦然(@InheritInverseConfiguration)。

3.6更新现有的bean实例

在某些情况下,您需要的映射不是创建目标类型的新实例,而是更新该类型的现有实例。这种映射可以通过为目标对象添加一个参数并使用@MappingTarget标记该参数来实现。示例如下:

@Mapper
public interface CarMapper {
   void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}

updateCarFromDto()方法生成的代码将使用给定CarDto对象的属性更新传入的Car实例。可能只有一个参数被标记为映射目标。除了void之外,您还可以将方法的返回类型设置为目标参数的类型,这将导致生成的实现更新传递的映射目标并返回它。

3.7直接字段映射

MapStruct还支持没有getter /setter的公共字段的映射。如果MapStruct不能为属性找到合适的getter/setter方法,它将使用这些字段作为读/写访问器。

如果字段是public或public final,则将其视为读访问器。如果字段是静态的,则不将其视为读访问器。

只有当字段是公共的时,它才被视为写访问器。如果字段是final和/或静态的,则不将其视为写访问器。

public class Customer {

   private Long id;
   private String name;

   //为简洁起见,省略了getter和setter
}

public class CustomerDto {

   public Long id;
   public String customerName;
}

@Mapper
public interface CustomerMapper {

   CustomerMapper INSTANCE = Mappers.getMapper( CustomerMapper.class );

   @Mapping(target = "name", source = "customerName")
   Customer toCustomer(CustomerDto customerDto);

// @InheritInverseConfiguration 是 MapStruct 中的一个注解,用于继承相反的映射配置。它通常与  
   // @Mapping 注解一起使用,用于声明映射配置的相反关系。
   @InheritInverseConfiguration
   CustomerDto fromCustomer(Customer customer);
}

上述代码中Customer类中存在getter和setter方法,CustomerDto中不存在getter和setter方法。MapStruct生成实现类代码为

package com.example.convert;

import com.example.model.Customer;
import com.example.model.CustomerDto;
import javax.annotation.Generated;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2023-06-29T19:57:23+0800",
   comments = "version: 1.5.5.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
)
public class CustomerMapperImpl implements CustomerMapper {

   @Override
   public Customer toCustomer(CustomerDto customerDto) {
       if ( customerDto == null ) {
           return null;
       }

       Customer customer = new Customer();

       customer.setName( customerDto.customerName );
       customer.setId( customerDto.id );

       return customer;
   }

   @Override
   public CustomerDto fromCustomer(Customer customer) {
       if ( customer == null ) {
           return null;
       }

       CustomerDto customerDto = new CustomerDto();

       customerDto.customerName = customer.getName();
       customerDto.id = customer.getId();

       return customerDto;
   }
}

3.8使用Builder

MapStruct还支持通过构建器映射不可变类型。在执行映射时,MapStruct检查所映射的类型是否有构造器。这是通过BuilderProvider SPI完成的。如果存在特定类型的生成器,则该生成器将用于映射。

  • 具有Builder的类

public class Person {

   private final String name;

   protected Person(Person.Builder builder) {
       this.name = builder.name;
   }

   public static Person.Builder builder() {
       return new Person.Builder();
   }

   public static class Builder {

       private String name;

       public Builder name(String name) {
           this.name = name;
           return this;
       }

       public Person create() {
           return new Person( this );
       }
   }
}

  • 创建映射

public interface PersonMapper {
   Person map(PersonDto dto);
}

  • Mapstruct生成的实现类代码

// GENERATED CODE
public class PersonMapperImpl implements PersonMapper {

   public Person map(PersonDto dto) {
       if (dto == null) {
           return null;
       }

       Person.Builder builder = Person.builder();

       builder.name( dto.getName() );

       return builder.create();
   }
}

3.9使用构造器

MapStruct支持使用构造函数映射目标类型。在进行映射时,MapStruct检查所映射的类型是否有构造器。如果没有构造器,那么MapStruct将查找单个可访问的构造器。当有多个构造函数时,执行以下操作来选择应该使用的构造函数:

  • 如果构造函数使用名为@Default的注释,它将被使用。

  • 如果存在单个公共构造函数,则将使用它来构造对象,而其他非公共构造函数将被忽略。

  • 如果存在无参数构造函数,则将使用它来构造对象,而忽略其他构造函数。

  • 如果有多个符合条件的构造函数,则会由于构造函数不明确而导致编译错误。为了消除歧义,可以使用名为@Default的注释。

public class Vehicle {

   protected Vehicle() { }

   // MapStruct将使用这个构造函数,因为它是一个单一的公共构造函数
   public Vehicle(String color) { }
}

public class Car {

   // MapStruct将使用这个构造函数,因为它是一个无参数的空构造函数
   public Car() { }

   public Car(String make, String color) { }
}

public class Truck {

   public Truck() { }

   // MapStruct将使用这个构造函数,因为它带有@Default注释
   @Default
   public Truck(String make, String color) { }
}

public class Van {

   // 使用这个类时会出现编译错误,因为MapStruct不能选择构造函数,需要有无参构造器或者@Default注释构造器
   
   public Van(String make) { }

   public Van(String make, String color) { }

}

当使用构造函数时,将使用构造函数的参数名并将其与目标属性匹配。当构造函数有一个名为@ConstructorProperties的注释时,该注释将用于获取参数的名称。

  • 具有构造函数的Person类

public class Person {

   private final String name;
   private final String surname;

   public Person(String name, String surname) {
       this.name = name;
       this.surname = surname;
   }
}

  • 创建映射

public interface PersonMapper {

   Person map(PersonDto dto);
}

  • MapStruct生成映射代码实现

// GENERATED CODE
public class PersonMapperImpl implements PersonMapper {

   public Person map(PersonDto dto) {
       if (dto == null) {
           return null;
       }

       String name;
       String surname;
       name = dto.getName();
       surname = dto.getSurname();

       Person person = new Person( name, surname );

       return person;
   }
}

3.10Map映射Bean

在某些情况下,需要从Map<String, ?>映射到特定bean。MapStruct通过使用目标bean属性(或通过mapping #source定义)从映射中提取值,提供了一种透明的方式来进行这种映射。这样的映射看起来像:

public class Customer {

   private Long id;
   private String name;

   //getters and setter omitted for brevity
}

@Mapper
public interface CustomerMapper {

   @Mapping(target = "name", source = "customerName")
   Customer toCustomer(Map<String, String> map);

}

MapStruct生成的映射实现类

// GENERATED CODE
public class CustomerMapperImpl implements CustomerMapper {

   @Override
   public Customer toCustomer(Map<String, String> map) {
       // ...
       if ( map.containsKey( "id" ) ) {
           customer.setId( Integer.parseInt( map.get( "id" ) ) );
       }
       if ( map.containsKey( "customerName" ) ) {
           customer.setName( map.get( "customerName" ) );
       }
       // ...
   }
}

4检索映射器

所谓检索映射器,其实就是如何在不同的框架中使用映射器。

4.1映射器工厂(没有依赖注入)

当不使用DI框架时,可以通过org.mapstruct.factory.Mappers类检索Mapper实例。只需调用getMapper()方法,传递映射器的接口类型以返回:

CarMapper mapper = Mappers.getMapper( CarMapper.class );

按照约定,mapper接口应该定义一个名为INSTANCE的成员,该成员包含mapper类型的单个实例:

@Mapper
public interface CarMapper {

   CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

   CarDto carToCarDto(Car car);
}@Mapper
public abstract class CarMapper {

   public static final CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

   CarDto carToCarDto(Car car);
}

他的模式使得客户端很容易使用mapper对象,而无需重复实例化新实例:

Car car = ...;
CarDto dto = CarMapper.INSTANCE.carToCarDto( car );

注意,由MapStruct生成的映射器是无状态和线程安全的,因此可以安全地从多个线程同时访问。

4.2使用依赖注入

这里可以参考3.1,是一种更常用的方法。

如果您正在使用依赖注入框架,如CDI (java EE的上下文和依赖注入)或Spring框架,建议通过依赖注入获得映射器对象,而不是像上面描述的那样通过Mappers类。为此,你可以通过@Mapper#componentModel指定生成mapper类的组件模型,或者使用配置选项中描述的处理器选项。

目前有对CDI和Spring的支持(后者要么通过自定义注解,要么使用JSR 330注解)。关于componentModel属性允许的值,请参见配置选项,这些值与mapstruct.defaultComponentModel处理器选项相同,常量在类MappingConstants.ComponentModel中定义。在这两种情况下,所需的注释都将被添加到生成的映射器实现类中,以便将相同的主题用于依赖注入。使用CDI的示例如下:

@Mapper(componentModel = MappingConstants.ComponentModel.CDI)
public interface CarMapper {

   CarDto carToCarDto(Car car);
}

生成的映射器实现将被标记为@ApplicationScoped注释,因此可以使用@Inject注释注入字段、构造函数参数等:

@Inject
private CarMapper mapper;

使用其他映射器类(参见调用其他映射器)的映射器将使用配置的组件模型获得这些映射器。因此,如果前面示例中的CarMapper使用了另一个映射器,那么这个映射器也必须是一个可注入的CDI bean。

4.3注入策略

使用依赖注入时,可以在字段注入和构造函数注入之间进行选择。这可以通过@Mapper或@MapperConfig注释提供注入策略来实现。

@Mapper(componentModel = MappingConstants.ComponentModel.CDI, uses = EngineMapper.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface CarMapper {
   CarDto carToCarDto(Car car);
}

如果MapStruct检测到需要为映射使用它的一个实例,那么生成的映射器将注入在uses属性中定义的类。当使用InjectionStrategy#CONSTRUCTOR时,构造函数将具有适当的注释,而字段则没有。当使用InjectionStrategy#FIELD时,注释是在字段本身上。目前,默认的注入策略是字段注入,但也可以通过Configuration选项进行配置。建议使用构造函数注入来简化测试。

5数据类型转换

映射属性在源对象和目标对象中并不总是具有相同的类型。例如,一个属性在源bean中可能是int类型,但在目标bean中可能是Long类型。

另一个例子是对其他对象的引用,这些对象应该映射到目标模型中的相应类型。例如,Car类可能有一个Person类型的属性驱动程序,在映射Car对象时需要将其转换为PersonDto对象。

在本节中,您将了解MapStruct如何处理此类数据类型转换。

5.1隐式类型转换

在许多情况下,MapStruct会自动处理类型转换。例如,如果一个属性在源bean中是int类型,而在目标bean中是String类型,那么生成的代码将分别通过调用string# valueOf(int)和Integer#parseInt(String)透明地执行转换。

目前自动应用以下转换:

  • 在所有Java基本数据类型和它们对应的包装类型之间,例如int和Integer, boolean和Boolean等。生成的代码是空感知的,即当将包装器类型转换为相应的原语类型时,将执行空检查。

  • 在所有Java基本数字类型和包装类型之间,例如在int和long或byte和Integer之间。

从较大的数据类型转换为较小的数据类型(例如从long到int)可能会导致值或精度损失。Mapper和MapperConfig注释有一个方法typeConversionPolicy来控制警告/错误。由于向后兼容性的原因,默认值是ReportingPolicy.IGNORE。

  • 在所有Java基本类型(包括它们的包装器)和String之间,例如在int和String或Boolean和String之间。可以指定java.text.DecimalFormat所转换的格式字符串。

@Mapper
public interface CarMapper {

   @Mapping(source = "price", numberFormat = "$#.00")
   CarDto carToCarDto(Car car);

   @IterableMapping(numberFormat = "$#.00")
   List<String> prices(List<Integer> prices);
}

  • 在大数类型之间(java.math.BigInteger, Java.math.BigDecimal)和Java基本类型(包括它们的包装器)以及String。可以指定java.text.DecimalFormat所理解的格式字符串。

@Mapper
public interface CarMapper {

   @Mapping(source = "power", numberFormat = "#.##E0")
   CarDto carToCarDto(Car car);

}

  • Java.util.Date/XMLGregorianCalendar和String之间。java.text.SimpleDateFormat所理解的格式字符串可以通过dateFormat选项指定,如下所示:

@Mapper
public interface CarMapper {

   @Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")
   CarDto carToCarDto(Car car);

   @IterableMapping(dateFormat = "dd.MM.yyyy")
   List<String> stringListToDateList(List<Date> dates);
}

5.2映射对象引用

通常,一个对象不仅具有基本属性,而且还引用其他对象。例如,Car类可以包含对Person对象(代表汽车的司机)的引用,该对象应该映射到由CarDto类引用的PersonDto对象。

在这种情况下,只需为引用的对象类型定义一个映射方法即可:

@Mapper
public interface CarMapper {

   CarDto carToCarDto(Car car);

   PersonDto personToPersonDto(Person person);
}

引用的映射可以参考MapStruct生成的转换代码的实现。

5.3控制嵌套bean映射

如上所述,MapStruct将根据源属性和目标属性的名称生成一个方法。不幸的是,在许多情况下,这些名称并不匹配。

@Mapping源或目标类型中的“.”符号可用于控制当名称不匹配时应该如何映射属性。在我们的示例存储库中有一个详细的示例来解释如何克服这个问题。

在最简单的场景中,有一个嵌套级别上的属性需要纠正。以属性fish为例,它在FishTankDto和FishTank中具有相同的名称。对于这个属性,MapStruct自动生成一个映射:FishDto fishToFishDto(Fish Fish)。MapStruct不可能知道偏离的属性类型。因此,这可以在映射规则中解决:@Mapping(target="fish.kind", source="fish.type")。这告诉MapStruct不要在这一层寻找名称类型,而是将其映射到类型。

@Mapper
public interface FishTankMapper {

   @Mapping(target = "fish.kind", source = "fish.type")
   @Mapping(target = "fish.name", ignore = true)
   @Mapping(target = "ornament", source = "interior.ornament")
   @Mapping(target = "material.materialType", source = "material")
   @Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName")
   FishTankDto map( FishTank source );
}

可以使用 java() 表达式,你可以在映射过程中执行一些自定义的 Java 代码逻辑,constant为目标对象属性指定常量值。

@Mapper
public interface FishTankMapperWithDocument {

   @Mapping(target = "fish.kind", source = "fish.type")
   @Mapping(target = "fish.name", expression = "java(\"Jaws\")")
   @Mapping(target = "plant", ignore = true )
   @Mapping(target = "ornament", ignore = true )
   @Mapping(target = "material", ignore = true)
   @Mapping(target = "quality.document", source = "quality.report")
   @Mapping(target = "quality.document.organisation.name", constant = "NoIdeaInc" )
   FishTankWithNestedDocumentDto map( FishTank source );
}

在提供的代码中,expression = "java(\"Jaws\")"constant = "NoIdeaInc" 是 MapStruct 中的表达式和常量映射的用法,用于指定目标对象属性的值。

  • expression = "java(\"Jaws\")":这个表达式指定了目标对象的 fish.name 属性的值。使用 java() 表达式,你可以在映射过程中执行一些自定义的 Java 代码逻辑。在这个例子中,"Jaws" 是一个硬编码的值,表示鱼的名称将始终为 "Jaws"

  • constant = "NoIdeaInc":这个常量指定了目标对象的 quality.document.organisation.name 属性的值。使用 constant 配置项,你可以将目标对象属性的值直接设置为提供的常量值。在这个例子中,"NoIdeaInc" 是一个字符串常量,它将始终作为 quality.document.organisation.name 属性的值。

5.4调用自定义映射方法

有时映射并不直接,有些字段需要自定义逻辑。

下面的示例演示了如何将FishTank中的属性长度、宽度和高度映射到VolumeDto bean,它是FishTankWithVolumeDto的成员。VolumeDto包含属性volume和description。自定义逻辑是通过定义一个方法来实现的,该方法以鱼缸实例作为参数并返回一个VolumeDto。MapStruct将获取整个参数源并生成代码来调用自定义方法mapVolume,以便将鱼缸对象映射到目标属性卷。

其余的字段可以用常规的方式进行映射:使用通过@Mapping注释定义的映射。

public class FishTank {
   Fish fish;
   String material;
   Quality quality;
   int length;
   int width;
   int height;
}

public class FishTankWithVolumeDto {
   FishDto fish;
   MaterialDto material;
   QualityDto quality;
   VolumeDto volume;
}

public class VolumeDto {
   int volume;
   String description;
}

@Mapper
public abstract class FishTankMapperWithVolume {

   @Mapping(target = "fish.kind", source = "source.fish.type")
   @Mapping(target = "material.materialType", source = "source.material")
   @Mapping(target = "quality.document", source = "source.quality.report")
   @Mapping(target = "volume", source = "source")
   abstract FishTankWithVolumeDto map(FishTank source);

   VolumeDto mapVolume(FishTank source) {
       int volume = source.length * source.width * source.height;
       String desc = volume < 100 ? "Small" : "Large";
       return new VolumeDto(volume, desc);
   }
}

注意@Mapping注释,其中source字段等于"source",表示方法映射(FishTank源)中的参数名称source本身,而不是FishTank中的(目标)属性。

5.5调用其他映射器

除了在相同的映射器类型上定义的方法之外,MapStruct还可以调用在其他类中定义的映射方法,无论是由MapStruct生成的映射器还是手写的映射方法。

例如,Car类可能包含一个属性manufacturingDate,而对应的DTO属性是String类型。为了映射这个属性,你可以像这样实现一个映射器类:

public class DateMapper {

   public String asString(Date date) {
       return date != null ? new SimpleDateFormat( "yyyy-MM-dd" )
           .format( date ) : null;
   }

   public Date asDate(String date) {
       try {
           return date != null ? new SimpleDateFormat( "yyyy-MM-dd" )
               .parse( date ) : null;
       }
       catch ( ParseException e ) {
           throw new RuntimeException( e );
       }
   }
}

在CarMapper接口的@Mapper注释中,像这样引用DateMapper类:

@Mapper(uses=DateMapper.class)
public interface CarMapper {

   CarDto carToCarDto(Car car);
}

在为carToCarDto()方法的实现生成代码时,MapStruct将查找一个将Date对象映射到String的方法,在DateMapper类中找到它,并生成一个调用asString()来映射manufacturingDate属性。

生成的映射器使用为它们配置的组件模型检索引用的映射器。例如,如果CDI被用作CarMapper的组件模型,DateMapper也必须是CDI bean。当使用默认组件模型时,任何要被MapStruct生成的映射器引用的手写映射器类必须声明一个公共的无参数构造函数才能被实例化。

5.6基于限定符的映射方法选择

在许多情况下,需要映射具有不同行为的具有相同方法签名(除了名称)的方法。MapStruct有一个方便的机制来处理这种情况:@Qualifier (org.mapstruct.Qualifier)。“qualifier”是用户可以编写的自定义注释,“粘贴”到映射方法上,该映射方法作为使用的映射器包含,可以在bean属性映射中引用。多个限定符可以“粘在”一个方法和映射上。

下面举一个例子,存在将String title翻译为英语或者德语两种映射方法。

public class Titles {

   public String translateTitleEG(String title) {
       // some mapping logic
   }

   public String translateTitleGE(String title) {
       // some mapping logic
   }
}

还有一个使用这个手写映射器的映射器,其中源和目标有一个属性“title”,应该被映射:

@Mapper( uses = Titles.class )
public interface MovieMapper {

    GermanRelease toGerman( OriginalRelease movies );

}

如果不使用限定符,这将导致模棱两可的映射方法错误,因为找到了2个限定方法(translateTitleEG, translateTitleGE),而MapStruct将没有提示选择哪一个。

输入限定符方法:

import org.mapstruct.Qualifier;

@Qualifier
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface TitleTranslator {
}

并且,一些限定符指示使用哪个翻译器将源语言映射到目标语言:

import org.mapstruct.Qualifier;

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface EnglishToGerman {
}import org.mapstruct.Qualifier;

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface GermanToEnglish {
}

请注意目标TitleTranslator在类型级别,英语到德语,德语到英语的方法级别!

然后,使用限定符,映射看起来像这样:

@Mapper( uses = Titles.class )
public interface MovieMapper {

    @Mapping( target = "title", qualifiedBy = { TitleTranslator.class, EnglishToGerman.class } )
    GermanRelease toGerman( OriginalRelease movies );

}@TitleTranslator
public class Titles {

   @EnglishToGerman
   public String translateTitleEG(String title) {
       // some mapping logic
   }

   @GermanToEnglish
   public String translateTitleGE(String title) {
       // some mapping logic
   }
}

在许多情况下,声明一个新的注释来帮助选择过程可能会超出您的预期。对于这些情况,MapStruct有@Named注释。这个注释是一个预定义的限定符(带有@Qualifier本身的注释),可以用来命名一个Mapper,或者更直接地通过它的值命名一个映射方法。上面的相同示例看起来像:

@Named("TitleTranslator")
public class Titles {

   @Named("EnglishToGerman")
   public String translateTitleEG(String title) {
       // some mapping logic
   }

   @Named("GermanToEnglish")
   public String translateTitleGE(String title) {
       // some mapping logic
   }
}@Mapper( uses = Titles.class )
public interface MovieMapper {

    @Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } )
    GermanRelease toGerman( OriginalRelease movies );

}

6集合映射

集合类型(List、Set等)的映射与映射bean类型的方式相同,即通过在映射器接口中定义具有所需源和目标类型的映射方法。MapStruct支持来自Java集合框架的大量可迭代类型。

生成的代码将包含一个循环,该循环遍历源集合,转换每个元素并将其放入目标集合。如果在给定的映射器或它使用的映射器中找到集合元素类型的映射方法,则调用该方法来执行元素转换。或者,如果存在源元素类型和目标元素类型的隐式转换,则将调用此转换例程。示例如下:

@Mapper
public interface CarMapper {

   Set<String> integerSetToStringSet(Set<Integer> integers);

   List<CarDto> carsToCarDtos(List<Car> cars);

   CarDto carToCarDto(Car car);
}

生成的integerSetToStringSet实现为每个元素执行从Integer到String的转换,而生成的carsToCarDtos()方法为每个包含的元素调用了carToCarDto()方法,如下所示:

//GENERATED CODE
@Override
public Set<String> integerSetToStringSet(Set<Integer> integers) {
   if ( integers == null ) {
       return null;
   }

   Set<String> set = new LinkedHashSet<String>();

   for ( Integer integer : integers ) {
       set.add( String.valueOf( integer ) );
   }

   return set;
}

@Override
public List<CarDto> carsToCarDtos(List<Car> cars) {
   if ( cars == null ) {
       return null;
   }

   List<CarDto> list = new ArrayList<CarDto>();

   for ( Car car : cars ) {
       list.add( carToCarDto( car ) );
   }

   return list;
}

注意,当映射bean的集合类型属性时,MapStruct将寻找具有匹配参数和返回类型的集合映射方法,例如从Car#passengers(类型为List<Person>)到CarDto#passengers(类型为List<PersonDto>)。

//GENERATED CODE
carDto.setPassengers( personsToPersonDtos( car.getPassengers() ) );
...

一些框架和库只公开JavaBeans的getter,而没有集合类型属性的setter。使用JAXB从XML模式生成的类型默认情况下遵循此模式。在这种情况下,生成的用于映射这样一个属性的代码调用它的getter并添加所有映射的元素:

//GENERATED CODE
carDto.getPassengers().addAll( personsToPersonDtos( car.getPassengers() ) );
...

6.1Map映射

public interface SourceTargetMapper {

   @MapMapping(valueDateFormat = "dd.MM.yyyy")
   Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}

与可迭代映射类似,生成的代码将遍历源映射,转换每个值和键(通过隐式转换或调用另一个映射方法),并将它们放入目标映射:

//GENERATED CODE
@Override
public Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> source) {
   if ( source == null ) {
       return null;
   }

   Map<Long, Date> map = new LinkedHashMap<Long, Date>();

   for ( Map.Entry<String, String> entry : source.entrySet() ) {

       Long key = Long.parseLong( entry.getKey() );
       Date value;
       try {
           value = new SimpleDateFormat( "dd.MM.yyyy" ).parse( entry.getValue() );
       }
       catch( ParseException e ) {
           throw new RuntimeException( e );
       }

       map.put( key, value );
   }

   return map;
}

6.2用于收集映射的实现类型

当可迭代或map映射方法声明接口类型为返回类型时,它的一个实现类型将在生成的代码中实例化。下表显示了支持的接口类型及其相应的实现类型,如在生成的代码中实例化:

Interface typeImplementation typeIterableArrayListCollectionArrayListListArrayListSetLinkedHashSetSortedSetTreeSetNavigableSetTreeSetMapLinkedHashMapSortedMapTreeMapNavigableMapTreeMapConcurrentMapConcurrentHashMapConcurrentNavigableMapConcurrentSkipListMap

7.枚举映射

感觉用处不大。

8.高级映射选项

8.1默认值和常量

可以指定默认值,以便在相应的源属性为空时将预定义值设置为目标属性。可以指定常量以在任何情况下设置这样的预定义值。默认值和常量被指定为String值。当目标类型为原语类型或盒装类型时,String值为文字值。在这种情况下,只要它们是有效的文字,就允许使用位/八进制/十进制/十六进制模式。在所有其他情况下,常量或默认值都要通过内置转换或调用其他映射方法进行类型转换,以匹配目标属性所需的类型。

带有常量的映射不能包含对源属性的引用。下面的例子展示了一些使用默认值和常量的映射:

@Mapper(uses = StringListMapper.class)
public interface SourceTargetMapper {

   SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

   @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
   @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
   @Mapping(target = "stringConstant", constant = "Constant Value")
   @Mapping(target = "integerConstant", constant = "14")
   @Mapping(target = "longWrapperConstant", constant = "3001")
   @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
   @Mapping(target = "stringListConstants", constant = "jack-jill-tom")
   Target sourceToTarget(Source s);
}

如果s.getStringProp() == null,则目标属性stringProperty将被设置为"undefined",而不是应用s.getStringProp()中的值。如果s.getLongProperty() == null,则目标属性longProperty将被设置为-1。字符串“Constant Value”按原样设置为目标属性stringConstant。值“3001”被类型转换为目标属性longWrapperConstant的Long(包装器)类。日期属性还需要日期格式。常量“jack- gill -tom”演示了如何调用手工编写的类StringListMapper,将虚线分隔的列表映射到list <String>。

8.2表达式

通过表达式,可以包含来自多种语言的结构。

目前只支持Java作为一种语言。这个特性在调用构造函数时非常有用。整个源对象可在表达式中使用。应该注意只插入有效的Java代码:MapStruct不会在生成时验证表达式,但是在编译期间生成的类中会显示错误。

下面的例子演示了如何将两个源属性映射到一个目标:

@Mapper
public interface SourceTargetMapper {

   SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

   @Mapping(target = "timeAndFormat",
        expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
   Target sourceToTarget(Source s);
}

该示例演示了如何将源属性time和format组合成一个目标属性TimeAndFormat。请注意,指定完全限定包名是因为MapStruct不负责TimeAndFormat类的导入(除非在SourceTargetMapper中显式地使用它)。这可以通过在@Mapper注释上定义导入来解决。

imports org.sample.TimeAndFormat;

@Mapper( imports = TimeAndFormat.class )
public interface SourceTargetMapper {

   SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

   @Mapping(target = "timeAndFormat",
        expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
   Target sourceToTarget(Source s);
}

8.3默认表达式

默认表达式是默认值和表达式的组合。只有当源属性为空时才会使用它们。

适用于表达式的默认表达式也适用相同的警告和限制。只支持Java,并且MapStruct不会在生成时验证表达式。

下面的例子演示了在source属性不存在时如何使用默认表达式来设置值(例如is null):

imports java.util.UUID;

@Mapper( imports = UUID.class )
public interface SourceTargetMapper {

   SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

   @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
   Target sourceToTarget(Source s);
}

这个例子演示了如何使用defaultExpression来设置一个ID字段,如果源字段是空的,它可以用来从源对象中获取现有的sourceId,如果它是空的,或者创建一个新的ID。请注意,指定完全限定包名是因为MapStruct不负责UUID类的导入(除非在SourceTargetMapper中显式地使用它)。这可以通过在@Mapper注释上定义导入来解决。

8.4子类映射

当输入类型和结果类型都具有继承关系时,您可能希望将正确的实例化映射到匹配的实例化。假设苹果和香蕉都是水果的实例化。

@Mapper
public interface FruitMapper {

   @SubclassMapping( source = AppleDto.class, target = Apple.class )
   @SubclassMapping( source = BananaDto.class, target = Banana.class )
   Fruit map( FruitDto source );

}

如果只使用普通映射,AppleDto和BananaDto都将被制作成一个Fruit对象,而不是一个Apple和一个Banana对象。通过使用子类映射,appldtotoapple映射将用于AppleDto对象,而BananaDtoToBanana映射将用于BananaDto对象。如果你尝试映射一个葡萄到它仍然会把它变成一个水果。

如果Fruit是一个抽象类或接口,则会得到编译错误。

为了允许抽象类或接口的映射,你需要将subclassExhaustiveStrategy设置为RUNTIME_EXCEPTION,你可以在@MapperConfig, @Mapper或@BeanMapping注释中这样做。如果你将一个GrapeDto传递给一个IllegalArgumentException将被抛出,因为它不知道如何映射一个GrapeDto。为它添加缺失的(@SubclassMapping)将修复这个问题。

8.5确定结果类型

当结果类型具有继承关系时,选择映射方法(@Mapping)或工厂方法(@BeanMapping)可能会产生歧义。假设苹果和香蕉都是水果的实例化。

@Mapper( uses = FruitFactory.class )
public interface FruitMapper {

   @BeanMapping( resultType = Apple.class )
   Fruit map( FruitDto source );

}public class FruitFactory {

   public Apple createApple() {
       return new Apple( "Apple" );
   }

   public Banana createBanana() {
       return new Banana( "Banana" );
   }
}

那么,哪些水果必须在映射方法Fruit map(FruitDto source)中因式分解呢?香蕉还是苹果?这里是@BeanMapping#resultType派上用场的地方。它控制要选择的工厂方法,或者在没有工厂方法的情况下控制要创建的返回类型。

8.6控制 'null' 参数的映射结果

当映射方法的源参数等于null时,MapStruct提供了对要创建的对象的控制。默认情况下将返回null。

但是,通过指定nullValueMappingStrategy = nullValueMappingStrategy。@BeanMapping, @IterableMapping, @MapMapping上的RETURN_DEFAULT,或者全局上的@Mapper或@MapperConfig,映射结果可以被修改为返回空的默认值。这意味着:

  • Bean映射:将返回一个'空'的目标Bean,除了常量和表达式,它们将在出现时被填充。

  • Iterables / Arrays:将返回一个空的iterable。

  • Maps:将返回一个空的map。

这种策略以分层的方式起作用。在映射方法级别设置nullValueMappingStrategy将覆盖@Mapper#nullValueMappingStrategy, @Mapper#nullValueMappingStrategy将覆盖@MapperConfig#nullValueMappingStrategy。

8.7控制 'null' 集合或映射参数的映射结果

通过控制'null'参数的映射结果,可以控制当映射方法的源参数为null时应该如何构造返回类型。这适用于所有映射方法(bean、iterable或map映射方法)。

然而,MapStruct还提供了一种更专用的方式来控制集合/映射应该如何映射。例如,返回默认的(空的)集合/映射,但为bean返回null。

对于集合(可迭代对象),可以通过以下方式控制:

  • MapperConfig#nullValueIterableMappingStrategy

  • Mapper#nullValueIterableMappingStrategy

  • IterableMapping#nullValueMappingStrategy

对于Map,这可以通过以下方式进行控制:

  • MapperConfig#nullValueMapMappingStrategy

  • Mapper#nullValueMapMappingStrategy

  • MapMapping#nullValueMappingStrategy

8.8条件映射

条件映射是源状态检查的一种类型。不同之处在于,它允许用户编写自定义条件方法,这些方法将被调用来检查是否需要映射属性。

自定义条件方法是一个带有org.mapstruct.Condition注释并返回布尔值的方法。

例如,如果你只想映射一个字符串属性,当它不是' null ',它不是空的,那么你可以这样做:

@Mapper
public interface CarMapper {

   CarDto carToCarDto(Car car);

   @Condition
   default boolean isNotEmpty(String value) {
       return value != null && !value.isEmpty();
   }
}

生成的映射器如下所示:

// GENERATED CODE
public class CarMapperImpl implements CarMapper {

   @Override
   public CarDto carToCarDto(Car car) {
       if ( car == null ) {
           return null;
       }

       CarDto carDto = new CarDto();

       if ( isNotEmpty( car.getOwner() ) ) {
           carDto.setOwner( car.getOwner() );
       }

       // Mapping of other properties

       return carDto;
   }
}

如果有一个自定义的@Condition方法适用于该属性,那么它将优先于bean本身的状态检查方法。

除了source属性的值之外,用@Condition注释的方法也可以将source参数作为输入。

9重用映射配置

9.1逆映射

在双向映射的情况下,例如从实体到DTO和从DTO到实体,正向方法和反向方法的映射规则通常是相似的,可以简单地通过交换源和目标来反转。

使用@InheritInverseConfiguration注释来指示一个方法应该继承对应的反向方法的反向配置。

在下面的示例中,不需要手动编写逆映射。考虑一个有几个映射的情况,因此编写逆映射可能很麻烦,而且容易出错。

@Mapper
public interface CarMapper {

   @Mapping(target = "seatCount", source = "numberOfSeats")
   CarDto carToDto(Car car);

   @InheritInverseConfiguration
   @Mapping(target = "numberOfSeats", ignore = true)
   Car carDtoToCar(CarDto carDto);
}

9.2共享配置

MapStruct提供了通过指向带有@MapperConfig注释的中心接口来定义共享配置的可能性。要让映射器使用共享配置,需要在@Mapper#config属性中定义配置接口。

@MapperConfig注释具有与@Mapper注释相同的属性。没有通过@Mapper给出的任何属性都将从共享配置中继承。在@Mapper中指定的属性优先于通过引用的配置类指定的属性。列表属性(如uses)被简单地组合起来:

@MapperConfig(
   uses = CustomMapperViaMapperConfig.class,
   unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface CentralConfig {
}@Mapper(config = CentralConfig.class, uses = { CustomMapperViaMapper.class } )
// Effective configuration:
// @Mapper(
//     uses = { CustomMapperViaMapper.class, CustomMapperViaMapperConfig.class },
//     unmappedTargetPolicy = ReportingPolicy.ERROR
// )
public interface SourceTargetMapper {
 ...
}

持有@MapperConfig注释的接口也可以声明映射方法的原型,这些方法可以用来继承方法级映射注释。这样的原型方法不打算作为映射器API的一部分来实现或使用。

@MapperConfig(
   uses = CustomMapperViaMapperConfig.class,
   unmappedTargetPolicy = ReportingPolicy.ERROR,
   mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG
)
public interface CentralConfig {

   // 不打算生成,但要携带可继承的映射注释:
   @Mapping(target = "primaryKey", source = "technicalKey")
   BaseEntity anyDtoToEntity(BaseDto dto);
}@Mapper(config = CentralConfig.class, uses = { CustomMapperViaMapper.class } )
public interface SourceTargetMapper {

   @Mapping(target = "numberOfSeats", source = "seatCount")
   // 额外继承自CentralConfig,因为Car扩展了BaseEntity, CarDto扩展了BaseDto:
   // @Mapping(target = "primaryKey", source = "technicalKey")
   Car toCar(CarDto car)
}

10自定义映射

有时需要在某些映射方法之前或之后应用自定义逻辑。MapStruct为此提供了两种方法:decorator允许对特定映射方法进行类型安全的自定义,而before-mapping和after-mapping生命周期方法允许对给定源或目标类型的映射方法进行通用的自定义。

10.1使用装饰器进行映射定制

在某些情况下,可能需要自定义生成的映射方法,例如,在目标对象中设置一个不能由生成的方法实现设置的附加属性。MapStruct使用装饰器支持这个需求。

要将装饰器应用到映射器类,请使用@DecoratedWith注释指定它。

@Mapper
@DecoratedWith(PersonMapperDecorator.class)
public interface PersonMapper {

   PersonMapper INSTANCE = Mappers.getMapper( PersonMapper.class );

   PersonDto personToPersonDto(Person person);

   AddressDto addressToAddressDto(Address address);
}

装饰器必须是被装饰映射器类型的子类型。你可以使它成为一个抽象类,它只允许实现那些你想自定义的映射器接口的方法。对于所有未实现的方法,将使用默认生成例程生成对原始映射器的简单委托。

下面显示的PersonMapperDecorator自定义了personToPersonDto()。它设置了一个在映射的源类型中不存在的附加属性。addressToAddressDto()方法不是自定义的。

public abstract class PersonMapperDecorator implements PersonMapper {

   private final PersonMapper delegate;

   public PersonMapperDecorator(PersonMapper delegate) {
       this.delegate = delegate;
   }

   @Override
   public PersonDto personToPersonDto(Person person) {
       PersonDto dto = delegate.personToPersonDto( person );
       dto.setFullName( person.getFirstName() + " " + person.getLastName() );
       return dto;
   }
}

该示例展示了如何选择性地使用生成的默认实现注入委托,并在自定义装饰器方法中使用此委托。

对于componentModel = "default"的映射器,定义一个带单个参数的构造函数,该参数接受被修饰的映射器的类型。

当使用组件模型spring或jsr330时,这需要以不同的方式处理。

10.1.1使用Spring组件模型的装饰器

当在具有组件模型spring的映射器上使用@DecoratedWith时,生成的原始映射器的实现将使用spring注释@Qualifier(“delegate”)进行注释。要在你的装饰器中自动装配那个bean,也要添加那个限定符注释:

public abstract class PersonMapperDecorator implements PersonMapper {

    @Autowired
    @Qualifier("delegate")
    private PersonMapper delegate;

    @Override
    public PersonDto personToPersonDto(Person person) {
        PersonDto dto = delegate.personToPersonDto( person );
        dto.setName( person.getFirstName() + " " + person.getLastName() );

        return dto;
    }
}

生成的扩展装饰器的类使用Spring的@Primary注释。要在应用程序中自动配置装饰映射器,不需要做任何特别的事情:

@Autowired
private PersonMapper personMapper; // injects the decorator, with the injected original mapper

10.1.2使用JSR 330组件模型的装饰器

JSR 330没有指定限定符,只允许指定bean的名称。因此,原始mapper的生成实现用@Named("full -qualified-name-of-generated-implementation")注释(请注意,当使用装饰器时,mapper实现的类名以下划线结尾)。要将该bean注入到你的装饰器中,请在delegate字段中添加相同的注释:

public abstract class PersonMapperDecorator implements PersonMapper {

   @Inject
   @Named("org.examples.PersonMapperImpl_")
   private PersonMapper delegate;

   @Override
   public PersonDto personToPersonDto(Person person) {
       PersonDto dto = delegate.personToPersonDto( person );
       dto.setName( person.getFirstName() + " " + person.getLastName() );

       return dto;
   }
}

与其他组件模型不同,使用站点必须知道一个映射器是否被装饰过,对于装饰过的映射器,必须添加无参数的@Named注释来选择要注入的装饰器:

@Inject
@Named
private PersonMapper personMapper; // injects the decorator, with the injected original mapper

10.2使用before-mapping和after-mapping方法的映射自定义

当涉及到定制映射器时,装饰器可能并不总是适合需要。例如,如果您不仅需要为几个选定的方法执行定制,而且需要为映射特定超类型的所有方法执行定制:在这种情况下,您可以使用在映射开始之前或映射完成之后调用的回调方法。

回调方法可以在抽象映射器本身中实现,也可以在mapper #uses中的类型引用中实现,或者在作为@Context参数使用的类型中实现。

@Mapper
public abstract class VehicleMapper {

   @BeforeMapping
   protected void flushEntity(AbstractVehicle vehicle) {
       // I would call my entity manager's flush() method here to make sure my entity
       // is populated with the right @Version before I let it map into the DTO
   }

   @AfterMapping
   protected void fillTank(AbstractVehicle vehicle, @MappingTarget AbstractVehicleDto result) {
       result.fuelUp( new Fuel( vehicle.getTankCapacity(), vehicle.getFuelType() ) );
   }

   public abstract CarDto toCarDto(Car car);
}

// Generates something like this:
public class VehicleMapperImpl extends VehicleMapper {

   public CarDto toCarDto(Car car) {
       flushEntity( car );

       if ( car == null ) {
           return null;
       }

       CarDto carDto = new CarDto();
       // attributes mapping ...

       fillTank( car, carDto );

       return carDto;
   }
}

如果@BeforeMapping / @AfterMapping方法有参数,只有当方法的返回类型(如果非void)可赋值给映射方法的返回类型,并且所有参数都可以由映射方法的源参数或目标参数赋值时,才会生成方法调用:

  • 带@MappingTarget注释的参数用映射的目标实例填充。

  • 带@TargetType注释的参数用映射的目标类型填充。

  • 带@Context注释的参数用映射方法的上下文参数填充。

  • 任何其他参数都使用映射的源参数填充。

  • 对于非空方法,如果方法调用的返回值不为空,则作为映射方法的结果返回。

与映射方法一样,可以为映射前/映射后方法指定类型参数。

@Mapper
public abstract class VehicleMapper {

   @PersistenceContext
   private EntityManager entityManager;

   @AfterMapping
   protected <T> T attachEntity(@MappingTarget T entity) {
       return entityManager.merge(entity);
   }

   public abstract CarDto toCarDto(Car car);
}

// Generates something like this:
public class VehicleMapperImpl extends VehicleMapper {

   public CarDto toCarDto(Car car) {
       if ( car == null ) {
           return null;
       }

       CarDto carDto = new CarDto();
       // attributes mapping ...

       CarDto target = attachEntity( carDto );
       if ( target != null ) {
           return target;
       }

       return carDto;
   }
}

所有可以应用于映射方法的映射前/映射后方法都将被使用。可以使用基于限定符的映射方法选择来进一步控制可以选择哪些方法,不可以选择哪些方法。为此,限定符注释需要应用于before/after方法,并在BeanMapping#qualifiedBy或IterableMapping#qualifiedBy中引用。

方法调用的顺序主要由它们的变体决定:

没有@MappingTarget参数的@BeforeMapping方法在对源参数进行空检查和构造新的目标bean之前被调用。

带有@MappingTarget参数的@BeforeMapping方法在构造新的目标bean之后被调用。

@AfterMapping方法在映射方法的末尾在最后一个返回语句之前被调用。

在这些组中,方法调用按其定义位置排序:

  1. 在@Context参数上声明的方法,按参数顺序排序。

  2. 在映射器本身中实现的方法。

  3. Mapper#中引用的类型的方法按照注释中的类型声明的顺序使用()。

  4. 在一个类型中声明的方法在其超类型中声明的方法之后使用。

重要提示:不能保证在一个类型中声明的方法顺序,因为它取决于编译器和处理环境实现。当使用构建器时,@AfterMapping注释方法必须将构建器作为@MappingTarget注释参数,以便该方法能够修改将要构建的对象。当@AfterMapping注释的方法范围完成时,将调用构建方法。如果真正的目标被用作@MappingTarget注释参数,MapStruct将不会调用@AfterMapping注释方法。


一文读懂MapStruct 1.5.5.Final的评论 (共 条)

分享到微博请遵守国家法律