MapStruct 1.5.5.Final
前言
这是 MapStruct 的参考文档,MapStruct 是一个基于注解处理器(annotation processor)的类转换器,它具有类型安全、高性能、没有其他依赖实现对象转换的特点。
1 介绍
MapStruct 是一个 Java 注解处理器,用于生成类型安全的 bean 映射类。
你所要做的就是定义一个 mapper
接口,在接口中声明所需要的映射方法。在编译期间,MapStruct 将生成这个接口的实现类。这个实现类基于纯 Java 方法,来执行源对象和目标对象的映射。
和手写映射代码相比,这种冗长且容易出错的代码,MapStruct 会自动生成,从而节省了时间。根据约定优于配置的原则,MapStruct 会使用合理的默认值,来生成这些转换代码,同时,还允许按照自己的方式,来配置或者实现特殊的行为。
与动态映射框架相比,MapStruct 具有以下优点:
- 通过普通方法调用,代替反射,执行速度更快;
- 编译时类型安全:只能映射相互映射的对象和属性,不会出现将一个
OrderEntity
映射到CustomerDTO
这种意外情况。 - 当出现下面情况下,在构建的时候,会有清楚的错误报告:
- 映射不完整(并非所有目标属性都被映射)
- 映射不正确(找不到适当的映射方法或者类型转换)
2 安装
MapStruct 是一个基于 JSR 269 的 Java 注释处理器,因此可以在命令行构建(javac、Ant、Maven 等)中使用,也可以在 IDE 中使用。
它包含以下组件:
org.mapstruct:mapstruct
:包含所需要的注解,例如@Mapper
org.mapstruct:mapstruct-processor
:包含生成 mapper 实现类的注解处理器
2.1 Apache Maven
对于基于 Maven 的项目,在 pom 文件中添加以下内容:
例1:Maven
...
<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>
...
2.2 Gradle
在 Gradle 构建文件中添加如下:
例2:Gradle
...
plugins {
...
id "com.diffplug.eclipse.apt" version "3.26.0" // Only for Eclipse
}
dependencies {
...
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
// If you are using mapstruct in test code
testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}
...
可以在 Github 中的 [mapstruct-example](mapstruct-examples/mapstruct-on-gradle at main · mapstruct/mapstruct-examples (github.com)) 查看完整的例子。
2.3 Apache Ant
在 build.xml
文件中,添加如下 javac
任务配置:
其中的路径需要按照你项目的结构来调整。
例3:Ant
...
<javac
srcdir="src/main/java"
destdir="target/classes"
classpath="path/to/mapstruct-1.5.5.Final.jar">
<compilerarg line="-processorpath path/to/mapstruct-processor-1.5.3.Final.jar"/>
<compilerarg line="-s target/generated-sources"/>
</javac>
...
可以在 Github 中的 [mapstruct-example](mapstruct-examples/mapstruct-on-gradle at main · mapstruct/mapstruct-examples (github.com)) 查看完整的例子。
2.4 配置选项
MapStruct 代码生成器可以使用注解处理器选项(annotation processor options)进行配置。
当直接调用 javac
时,可以以 -Akey=value
的形式,传递给编译器。
当通过 Maven 使用 MapStruct 时,任何选项都可以通过在 Maven 处理器插件中,使用 compilerArgs
属性来配置传递。如下图所示:
例4:Maven
...
<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>
...
例5:Gradle
...
compileJava {
options.compilerArgs += [
'-Amapstruct.suppressGeneratorTimestamp=true',
'-Amapstruct.suppressGeneratorVersionInfoComment=true',
'-Amapstruct.verbose=true'
]
}
...
支持配置的属性
配置项 | 用途 | 默认值 |
---|---|---|
mapstruct. suppressGeneratorTimestamp | 如果设置为 true ,则禁止在生成的 Mapper 实现类上面的 @Generated 注解中添加时间戳 | false |
mapstruct.verbose | 如果设置为 true ,MapStruct 会打印其重要决策信息。需要注意的是,在使用 Maven是,还需要添加 showWarnings 这个属性,这是由于 maven-compiler-plugin 配置中的一个问题 | false |
mapstruct.suppressGeneratorVersionInfoComment | 如果设置为 true 的话,则禁止在生成的 Mapper 实现类上的 @Generated 注解中添加 comment 属性。该属性包含有关 MapStruct 版本和用于注释处理器的编译器信息 | false |
mapstruct.defaultComponentModel | 生成的 Mapper 实现类时的组件模型(Component Model)名称(请参见检索映射器)章节。 支持属性如下: - default :Mapper 不使用组件模型,通常通过 Mappers#getMapper(Class) 来检索实例- cdi :生成的转换器是一个 CDI 应用 bean,可以通过 @Inject 检索。 - spring :生成的映射器是一个 Spring 的单例 bean,可以通过 @Autowired 来检索。- jsr330 : 生成的映射器被 @Named 注解,可以通过 @Inject (javax.inject 或 jakarta.inject 包下,javax.inject 优先级更高) 来检索- jakarta :生成的映射器被 @Named 注解。还可以通过 @Mapper#componentModel() 来配置具体的转换器,且优先级更高 | default |
mapstruct.defaultInjectionStrategy | 通过 uses 属性使用的转换器,注入的方式。仅用于基于注解的组件模型,如 CDI、Spring、JSR 330。 支持配置的值如下: - field :通过属性的方式注入依赖 - constructor :将为实现类生成构造器,且通过构造器的方式注入依赖 当组件模型是 CDI 时,会生成一个默认的构造器。 该策略还可以通过 @Mapper#injectionStrategy() 来配置指定的 Mapper 接口,且优先级更高。 | field |
mapstruct.unmappedTargetPolicy | 当目标属性没有基于来源值的填充方法(例如:setXxx)时,MapStruct 的默认报告策略。 支持如下值:- Error :任何未映射的目标属性都将导致映射代码生成失败 - WARN :在编译阶段任何未映射的目标属性都会造成一个异常 -IGNORE :忽略未映射的目标属性 该策略还可以通过 @Mapper#unmappedTargetPolicy() 为具体的映射器指定,且优先级更高。除此之外,还可以通过 @BeanMapping#unmappedTargetPolicy() 未特定的 bean 映射指定策略,该配置优先级最高。 | WARN |
mapstruct.unmappedSourcePolicy | 当来源属性没有可以给目标属性填充值的方法时,MapStruct 的默认报告策略。 支持如下值:- Error :任何未映射的来源属性都将导致映射代码生成失败 - WARN :在编译阶段任何未映射的来源属性都会造成一个异常 -IGNORE :忽略未映射的来源属性 该策略还可以通过 @Mapper#unmappedSourcePolicy() 为具体的映射器指定,且优先级更高。除此之外,还可以通过 @BeanMapping#unmappedSourcePolicy() 未特定的 bean 映射指定策略,该配置优先级最高。 | WARN |
mapstruct. disableBuilders | 如果设置为 true,那么在进行映射时 MapStruct 将不会使用构造器模式。这相当于为所有映射器添加了 @Mapper(build = @Builder(disableBuilder = true)) 配置 | false |
2.5 将 MapStruct 与 Java 模块系统一起使用
模块系统(Module System):Java 9 的新特性
MapStruct 可以与 Java 9 及更高版本一起使用。
原文:To allow usage of the @Generated annotation java.annotation.processing.Generated (part of the java.compiler module) can be enabled.
这一段不知道该如何翻译。但可以说一下大概想法。
@Generated
JDK 11 之后被默认去除了,MapStruct 会根据运行环境,当 Java 11 及以后,会自动添加 javax.annotation-api
依赖包,从而使用该注解。
2.6 IDE 整合
Intellij
Intellij IDEA 中可以安装 MapStruct Support 插件,更好的使用 MapStruct。
该插件包含以下的特性:
target
、source
、expression
自动提示- 支持
target
、source
直接跳转属性定义的地方 - 查找
target
、source
属性的用法 - 支持重构
- 异常和快速修复
Eclipse
Eclipse 同样提供了 [MapStruct Eclipse Plugin] 插件,以方便 MapStruct 的使用。
包含以下特定:
target
和source
代码提示- 快速修复
3 定义一个 mapper
在本节,您将学习如何使用 MapStruct 定义 bean 映射器(mapper),以及可以配置哪些选项。
3.1 基本映射(Basic Mappings)
要创建一个映射器,只需要定义一个接口、需要的映射方法,及在该接口上面添加 org.mapstruct.Mapper
注解。
例6:通过
@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
注解指定其名称。
JavaBeans 规范中定义的属性名称,必须在
@Mapping
注解中指定,例如属性 seatCount 和其访问方法getSeatCount()
、setSeatCount()
当指定
@BeanMapping(ignoreByDefault = true)
时,则不会隐式的进行属性转换,这意味着必须通过@Mapping
指定所有映射关系,并且不会在缺少目标属性时发出任何警告。这允许忽略所有的除了@Mapping
注解之外的字段。流式(fluent) setters 也是支持的。所谓流式 setters 是指 setters 方法返回当前实例。
例如:
public Builder seatCount(int seatCount) { this.seatCount = seatCount; return this; }
为了更好的理解 MapStruct 的功能,来看一下 MapStruct 生成的 carToCarDto()
实现方法:
例7:通过
// 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
调用,而不是通过反射或者其他类似的方式。
如示例所示,生成的代码考虑了通过 @Mapping
指定的任何名称直接的映射。如果需要映射的属性在源实体和目标实体中类型是不同的,MapStruct 要么应用一个自动转换(例如,对于 price
属性的转换,请参考隐式类型转换),或者,调用/生成另一个映射方法(例如 driver
/engine
属性的转换,请参考转换对象引用)。 只有当源属性和目标属性是 Bean 的属性,并且它们本身是 Bean 或简单属性时,MapStruct 才会创建一个新的映射方法。比如,他们不是 Collection
或 Map
类型的属性。
对于元素类型相同的 Collection
集合,在执行映射时,会创建一个新的集合,并拷贝源对象的集合数据。对于元素类型不同的 Collection
集合,每个元素会单独映射,再添加到目标集合中。(详情可以参考集合映射)
MapStruct 会对源类型和目标类型中所有公开(public)属性进行映射,包括定义在父类中的属性。
3.2 Mapping 组合(实验性)
MapStruct 支持使用元注解。@Mapping
注解除了支持配置在方法上面,还可以配置在注解上面。这允许通过其他(用户定义)的注解来重复利用 @Mapping
注解。例如:
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "id", ignore = true)
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
@Mapping(target = "name", source = "groupName")
public @interface ToEntity { }
这可以在不需要具有公共基本类型的基础上描述实体。例如下面的示例中,ShelveEntity
和 BoxEntity
在 StorageMapper
中不共享公共基本类型。
@Mapper
public interface StorageMapper {
StorageMapper INSTANCE = Mappers.getMapper( StorageMapper.class );
@ToEntity
@Mapping( target = "weightLimit", source = "maxWeight")
ShelveEntity map(ShelveDto source);
@ToEntity
@Mapping( target = "label", source = "designation")
BoxEntity map(BoxDto source);
}
然而,他们必须要有一些共同的属性。@ToEntity
注解假设所有的目标类型(ShelveEntity
和 BoxEntity
)都包含属性:"id
"、"createDate
"、"name
"。此外,还假设所有的来源类型(ShelveDto
、BoxDto
)都有属性 "groupName
"。 这个概念也被称为“鸭子类型“,换而言之,如果它像鸭子一样嘎嘎叫,像鸭子一样走路,那么它可能就是一只鸭子。
这个功能还属于实验特性。当出现异常情况时,描述信息并不完善:会直接显示出现问题的方法,以及 @Mapping
中的相关值。 然而,不能直接显示这个组合的影响方面。这些信息“好像”是 @Mapping
直接出现在相关方法上。因此,用户应该谨慎地使用这个特性,特别是不确定一个属性是否一直存在。
一种类型更安全(但也会更加啰嗦)的方式是定义一个基本类或者接口,目标类和源类继承该类,并且使用 @InheritConfiguration
注解,实现相同的结果(请参考[Mapping 配置继承](#Mapping 配置继承))。
3.3 在转换类中添加自定义方法
在某些情况下,可能需要手动实现从一种类型映射为另一种类型的特性实现,这种实现是 MapStruct 无法生成的。处理这个问题的一种方式是在另一个类上实现自定义方法,然后由 MapStruct 生成的映射器来使用这个方法(请参考执行其他映射器)
当 Java 8 或者之后的版本后,也可以选择另一种方法:可以直接在映射接口(mapper interface)中实现自定义方法。生成映射代码时,当参数和返回类型与该方法相同,则将默认调用该方法。
假如,当 Person
映射为 PersonDto
时,需要一些特殊的逻辑,而而抓拍逻辑无法由 MapStruct 来生成。基于前面的例子,通过在转换器接口中定义转换方法的方式,实现这个要求:
例8:在转换接口中定义默认的自定义映射方法
@Mapper
public interface CarMapper {
@Mapping(...)
...
CarDto carToCarDto(Car car);
default PersonDto personToPersonDto(Person person) {
//hand-written mapping logic
}
}
MapStruct 生成的实现类中实现了 carToCarDto()
方法,在生成的这个方法中,当执行 driver
属性映射时,会执行手动实现的 personToPersonDto()
方法。
映射器除了接口的形式,也可以是抽象类的形式,可以直接在抽象映射器类中实现自定义方法。在这种情况下,MapStruct 会实现抽象类中的所有非抽象方法。这种方式比在接口中声明默认方法的优点是,可以在抽象类中声明其他属性。
在前面 Person
转换为 PersonDto
的特殊映射逻辑例子中,还可以像如下这样定义:
例9
@Mapper
public abstract class CarMapper {
@Mapping(...)
...
public abstract CarDto carToCarDto(Car car);
public PersonDto personToPersonDto(Person person) {
//hand-written mapping logic
}
}
MapStruct 将会生成 CarMapper
的子类,并且实现其中的抽象方法 carToCarDto()
。在生成的 carToCarDto()
代码中,映射 driver
属性时,会执行手动实现的 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
属性的转换。 当没有解决这种歧义问题时,将会报错。对于仅存在与给定的来源对象中的一个时,可以省略指定这个属性的来源参数,因为它可以自动确定。
WARNING
在使用 @Mapping
注解时,必须指定参数的来源属性。
INFO
多来源参数映射中,当所有来源参数都为 null
时,方法最终会返回 null
。 否则,会根据提供的源对象来生成目标对象。
MapStruct 还支持直接使用来源参数进行转换。
例11:直接引用来源参数的转换方法
@Mapper
public interface AddressMapper {
@Mapping(target = "description", source = "person.description")
@Mapping(target = "houseNumber", source = "hn")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}
在这个例子中,来源参数直接映射到目标对象。参数 hn
是一个非 bean 类型(在这个例子是 java.lang.Integer
)映射到 houseNumber
属性。
3.5 嵌套对象属性映射到目标属性
如果不想显式地命名来源对象中嵌套 bean 的属性,可以配置 target
参数为 .
。这将告诉 MapStruct 映射转换每一个嵌套 bean 的属性到目标对象中。下面是示例:
例12:使用
@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")
就是为了解决这个冲突的。
当多层级对象映射到平铺对象的时候(反之亦然 @InheritInverseConfiguration
),这个特性非常有用.
3.6 修改已经存在的对象实例
有的时候,需要执行映射时,不返回一个新的对象,而是更新现有的对象实例。这种可以通过将已经存在的目标对象,添加到映射方法的参数中,并用 @MappingTarget
注解标注。 下面是一个例子:
例13:更改方法
@Mapper
public interface CarMapper {
void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}
生成的 updateCarFromDto()
方法实现中,会使用给定的 CarDto
对象,来修改传递的 Car
实例。 这里可能只有一个参数标记为映射的目标对象。除了将映射方法的返回类型设置为 void
,还可以设置返回为目标对象的类型,这种情况下,生成的实现方法中,会修改传入的映射目标对象,并将其返回。这样可以支持流式调用映射方法。
当目标属性类型是 Collection
或者 Map
,当策略为 CollectionMappingStrategy.ACCESSOR_ONLY
时,将会先将集合清空(clear),再用源对象中的值填充。 除此之外,当策略为 CollectionMappingStrategy.ADDER_PREFERRED
或 CollectionMappingStrategy.TARGET_IMMUTABLE
时,目标属性的集合不会清空,且立即填充值。
3.7 直接访问属性的映射
MapStruct 同样支持 public
类型的字段(没有 getters/setters )进行映射。 当找不到这些属性的 getter/setter 方法时,会使用这些字段进行读写。
只有当一个属性为 public
或 public final
时,才可以作为读访问器。 如果字段是 static
类型的话,不能够作为读访问器。
只有当一个属性为 public
时,才能作为写访问器。 如果字段是 final
或者 static
时,不能够作为读访问器。
示例:
例14:类映射示例
public class Customer {
private Long id;
private String name;
//getters and setter omitted for brevity
}
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
CustomerDto fromCustomer(Customer customer);
}
对于上面的配置,生成的映射器如下所示:
例15:生成的类映射器示例
// GENERATED CODE
public class CustomerMapperImpl implements CustomerMapper {
@Override
public Customer toCustomer(CustomerDto customerDto) {
// ...
customer.setId( customerDto.id );
customer.setName( customerDto.customerName );
// ...
}
@Override
public CustomerDto fromCustomer(Customer customer) {
// ...
customerDto.id = customer.getId();
customerDto.customerName = customer.getName();
// ...
}
}
可以在 mapstruct-example-field-mapping 中查看完整的示例。
3.8 使用构造器
MapStruct 支持使用构造器来映射不可变的类型。在 MapStruct 执行映射时,会检查是否存在可以用于映射类型的构造器。 这是通过 BuilderProvider
SPI 来实现的,如果存在的话,则会使用该构造器来映射。
BuilderProvider
默认实现假设如下「这里可以认为是只有满足如下条件时,该类才会生效」:
- 该类型有一个无参公共静态构造器创建方法,该方法返回一个构造器。例如,
Person
有一个返回PersonBuilder
的公共静态方法。 - 构造器类中有一个无参的公共方法(构造方法),该方法返回其内部构造的类型。例如示例中
PersonBuilder
有一个返回Person
的方法。 - 当有多个构造方法时,MapStruct 将会寻找一个名为
build
的方法,如果存在就用该方法,否则创建一个编译异常。 - 特殊的构造方法,可以在
@BeanMapping
、@Mapper
或@MapperConfig
中提供的@Builder
来指定。 - 当有多个满足条件的构造器创建方法存在时,
DefaultBuilderProvider
SPI 将会抛出一个MoreThanOneBuilderCreationMethodException
异常。对于该异常,MapStruct 会在编译中写入「记录」警告,并且不适用任何生成器。
如果找到了这样子的构造器,MapStruct 会使用该类来执行映射(即使找到了该类提供的 setters)。MapStruct 会调用构造器的构造方法来生成映射代码。
WARNING
构造器检测的特性,可以通过 @Builder.disableBuilder
来关闭。当关闭后,MapStruct 将像以前一样使用常规的 getter/setter。
WARNING
对于构造器类型创建,还可以使用使用对象工厂。例如,在 PersonBuilder
中存在一个对象工厂时,将会使用这个工厂类来代替构造器创建方法
WARNING
构造器会影响 @BeforeMapping
和 @AfterMapping
行为,请参考章节 Mapping customization with before-mapping and after-mapping methods
定制化转换-转换前和转换后 来了解更多信息
例16:Person
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 );
}
}
}
例17:定义
public interface PersonMapper {
Person map(PersonDto dto);
}
例18:基于构造器来生成转换器实现类
// 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();
}
}
支持的构造器框架:
- Lombok:
需要将 Lombok 类放在单独的模块中。有关更多信息,可以查看 lombok#1538,并使用 MapStruct 设置 Lombok,请参阅 Lombok。
这里说的单独的模块,应该是 Lombok 之前的问题,现在已经解决,可以忽略。
- AutoValue
- Immutables:当注释处理器路径上存在 Immutables 时,默认使用
ImmutablesAccessorNamingStrategy
和ImmutablesBuilderProvider
。 - FreeBuilder:当注释处理器路径上存在 FreeBuilder 时,默认使用
FreeBuilderAccessorNamingStrategy
。当使用 FreeBuilder 时,应当遵循 JavaBean 约定,否则 MapStruct 将无法识别流利的 getters。 - 同样适用于自定义的构造器(手写构造器),前提构造器实现支持
BuilderProvider
定义的规则。否则,需要编写一个自定义的BuilderProvider
。
WARNING
如果想要禁用构造器,那么可以将 MapStruct 处理器选项 mapstruct.disablebuilders
传递给编译期。例如:-Amapstruct.disableBuilders=true
3.9 使用构造函数
MapStruct 支持使用构造函数来映射目标类型。当 MapStruct 执行映射时,会检查是否存在目标类型的构造器。 如果没有构造器,MapStruct 会寻找一个可访问的构造函数。当有多个构造函数时,将按照如下规则,选择一个构造函数来使用:
- 如果一个构造函数被
@Default
(任意包都可以,参考未列出注释) 注解标注,那么会使用该构造函数。 - 如果只存在一个公开的构造函数,则会使用它来构造对象,其他的非公开构造函数将被忽略。
- 如果存在无参构造函数,那么会用它来构造对象,而其他构造函数将被忽略。
- 如果存在多个符合条件的构造函数,优于模棱两可的构造函数将出现编译异常。为了打破歧义,可以使用
@Default
注解来标注。
例19:决定使用哪个构造函数
public class Vehicle {
protected Vehicle() { }
// MapStruct will use this constructor, because it is a single public constructor
public Vehicle(String color) { }
}
public class Car {
// MapStruct will use this constructor, because it is a parameterless empty constructor
public Car() { }
public Car(String make, String color) { }
}
public class Truck {
public Truck() { }
// MapStruct will use this constructor, because it is annotated with @Default
@Default
public Truck(String make, String color) { }
}
public class Van {
// There will be a compilation error when using this class because MapStruct cannot pick a constructor
public Van(String make) { }
public Van(String make, String color) { }
}
使用构造函数时,默认使用构造函数的参数名与目标属性匹配。 如果构造函数上有 @ConstructorProperties
(任意来源包,请参考未列出注释) 名称的注解时,则会使用该注解来获取参数名称。
INFO
当存在 @ObjectFactory
注解标注的对象工厂方法时,该方法比所有的构造函数优先级更高。在这种情况下,将不会使用目标对象的构造函数。
例20:具有构造函数参数的
public class Person {
private final String name;
private final String surname;
public Person(String name, String surname) {
this.name = name;
this.surname = surname;
}
}
例21:定义
public interface PersonMapper {
Person map(PersonDto dto);
}
例22:生成的转换器实现类
// 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.10 映射 Map 为 Bean
在某些情况下,需要将一个 Map<String, ???>
映射为一个指定的 Bean。 MapStruct 提供了一种方法,通过目标类的属性(或者通过 Mapping#source
定义),从 Map 中提取相应的值,来完成映射。例如:
例23:映射
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);
}
例24:映射
// 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" ) );
}
// ...
}
}
INFO
目前支持的不同类型之间的映射,以及使用 Mapper#uses
中定义的其他映射器,或者在映射器中自定义的方法,同样支持 Map 转换为 Bean。 例如,也可以从一个 Map<String, Integer>
转换为一个 Bean 对象,这就需要每个属性都将从 Integer
类型转换而来。
4 检索映射器
4.1 映射器工厂(非依赖注入)「Mappers Factory」
当不适用依赖注入的框架时,可以通过 org.mapstruct.factory.Mappers
类检索映射器实例。只需要调用 getMapper()
方法,并传入接口类型,则会返回相应的映射器实例。
例25:使用映射器工厂
CarMapper mapper = Mappers.getMapper( CarMapper.class );
按照惯例,映射器接口应该定义一个名为 INSTANCE
的属性,该属性保存着当前映射器类型的单个实例:
例26:声明一个转换器接口的实例
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
CarDto carToCarDto(Car car);
}
例27:声明一个转换器抽象类的实例
@Mapper
public abstract class CarMapper {
public static final CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
CarDto carToCarDto(Car car);
}
这种方式可以非常容易地使用映射器对象,而无需重复实例化新的实例:
例28:访问映射器
Car car = ...;
CarDto dto = CarMapper.INSTANCE.carToCarDto( car );
注意:由 MapStruct 生成的映射器是无状态且线程安全的,因此可以同时多线程访问。
4.2 使用依赖注入
如果你使用的是一个依赖注入的框架,例如 CDI 或者 Spring 框架,建议通过依赖注入的方式获取映射器对象,而不是像上面描述的那样通过 Mappers
类来获取。 为此,可以通过 @Mapper#componentModel
或者定义处理器属性(参考配置选项)指定生成的映射器类的组件模型(component model)。
目前支持 CDI 和 Spring(后者通过自定义注解或者使用 JSR 330 注解)。 请参阅配置选项中 componentModel
属性允许的值,该配置和 mapstruct.defaultComponentModel
一致,且具体的常量都定义在类 MappingConstants.ComponentModel
中。 在这两种情况下,所需的注解都将被添加到生成的转换器实现类中,以保证依赖注入的方式相同。下面展示了使用 CDI 的示例:
例29:使用
@Mapper(componentModel = MappingConstants.ComponentModel.CDI) public interface CarMapper {
CarDto carToCarDto(Car car);
}
生成的转换器实现类会被 @ApplicationScoped
注解标注,并且可以使用 @Inject
注解,通过属性或构造器注入。
例30:通过依赖注入的方式获取一个转换器
@Inject
private CarMapper mapper;
如果在一个映射器中使用其他的映射器(参考执行其他映射器),将会使用配置的组件模型来获取这些映射器的对象。所以这里如果上一个示例中 CarMapper
使用了另一个映射器,则该映射器也必须是一个可注入的 CDI bean。
4.3 注入策略
当使用依赖注入时,可以选择属性注入还是构造器注入。可以通过 @Mapper
或 @MapperConfig
注解来配置该注入策略。
例31:使用构造器注入
@Mapper(componentModel = MappingConstants.ComponentModel.CDI, uses = EngineMapper.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface CarMapper {
CarDto carToCarDto(Car car);
}
如果 MapStruct 检测到 uses 属性中定义了其他的映射器时,则会在生成的映射器实现类中注入这些类的实例。 当使用 InjectionStrategy#CONSTRUCTOR
策略时,将会在构造器上增加合适的注解,而不会添加到属性上面。 当使用 InjectionStrategy#FIELD
策略时,注解会被添加到属性本身上。默认的注入策略是属性注入,但是可以在配置选项中进行配置。 推荐使用构造器注入,来简化测试。
INFO
对于抽象类映射器,应当使用 setter 注入策略