然而,他们必须要有一些共同的属性。
@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
属性的转换。 当没有解决这种歧义问题时,将会报错。对于仅存在与给定的来源对象中的一个时,可以省略指定这个属性的来源参数,因为它可以自动确定。
注意
在使用
@Mapping
注解时,必须指定参数的来源属性。
相关信息
多来源参数映射中,当所有来源参数都为
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:使用 `.` 来配置注解中的 target
@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
open in new window
中查看完整的示例。
3.8 使用构造器
MapStruct 支持使用构造器来映射不可变的类型。在 MapStruct 执行映射时,会检查是否存在可以用于映射类型的构造器。 这是通过
BuilderProvider
SPI 来实现的,如果存在的话,则会使用该构造器来映射。
BuilderProvider
默认实现假设如下「
这里可以认为是只有满足如下条件时,该类才会生效
」:
-
该类型有一个无参公共静态构造器创建方法,该方法返回一个构造器。例如,
Person
有一个返回
PersonBuilder
的公共静态方法。
-
构造器类中有一个无参的公共方法(构造方法),该方法返回其内部构造的类型。例如示例中
PersonBuilder
有一个返回
Person
的方法。
-
当有多个构造方法时,MapStruct 将会寻找一个名为
build
的方法,如果存在就用该方法,否则创建一个编译异常。
-
特殊的构造方法,可以在
@BeanMapping
、
@Mapper
或
@MapperConfig
中提供的
@Builder
来指定。
-
当有多个满足条件的构造器创建方法存在时,
DefaultBuilderProvider
SPI 将会抛出一个
MoreThanOneBuilderCreationMethodException
异常。对于该异常,MapStruct 会在编译中写入「
记录
」警告,并且不适用任何生成器。
如果找到了这样子的构造器,MapStruct 会使用该类来执行映射(即使找到了该类提供的 setters)。MapStruct 会调用构造器的构造方法来生成映射代码。
注意
构造器检测的特性,可以通过
@Builder.disableBuilder
来关闭。当关闭后,MapStruct 将像以前一样使用常规的 getter/setter。
注意
对于构造器类型创建,还可以使用使用对象工厂。例如,在
PersonBuilder
中存在一个对象工厂时,将会使用这个工厂类来代替构造器创建方法
注意
构造器会影响
@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:定义 Person 转换器
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 之前的问题,现在已经解决,可以忽略。
-
AutoValue
-
Immutables:当注释处理器路径上存在 Immutables 时,默认使用
ImmutablesAccessorNamingStrategy
和
ImmutablesBuilderProvider
。
-
FreeBuilder:当注释处理器路径上存在 FreeBuilder 时,默认使用
FreeBuilderAccessorNamingStrategy
。当使用 FreeBuilder 时,应当遵循 JavaBean 约定,否则 MapStruct 将无法识别流利的 getters。
-
同样适用于自定义的构造器(手写构造器),前提构造器实现支持
BuilderProvider
定义的规则。否则,需要编写一个自定义的
BuilderProvider
。
注意
如果想要禁用构造器,那么可以将 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
(任意来源包,请参考
未列出注释
) 名称的注解时,则会使用该注解来获取参数名称。
相关信息
当存在
@ObjectFactory
注解标注的对象工厂方法时,该方法比所有的构造函数优先级更高。在这种情况下,将不会使用目标对象的构造函数。
例20:具有构造函数参数的 Person
public class Person {
private final String name;
private final String surname;
public Person(String name, String surname) {
this.name = name;
this.surname = surname;
例21:定义 Person 的构造函数转换器
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:映射 Map 为 Bean 的示例类
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:映射 Map 为 Bean 的转换器实现类
// 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" ) );
// ...
相关信息
目前支持的不同类型之间的映射,以及使用
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:使用 CDI 组件模型的转换器
@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 {