文章目录
3、定义映射器
3.1 基本映射
要创建一个映射器,只需定义一个带有所需映射方法的Java接口,并使用org.mapstruct.Mapper注解进行注解:
示例1. 定义映射器的Java接口
@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注解指定其名称。
- 必须在@Mapping注解中指定属性名称,该名称定义在JavaBeans规范中,例如对于具有访问器方法getSeatCount()和setSeatCount()的属性,名称为seatCount。
- 通过@BeanMapping(ignoreByDefault =
true),默认行为将是显式映射,意味着所有映射都必须通过@Mapping指定,并且在缺少目标属性时不会发出警告。这允许忽略所有字段,除了通过@Mapping显式定义的字段。 - 还支持流畅的设置器(Fluent Setters)。流畅的设置器是指返回与正在修改的类型相同类型的设置器。
例如:
public Builder seatCount(int seatCount) {
this.seatCount = seatCount;
return this;
}
3.2 映射组合(实验性功能)
MapStruct支持使用元注解(meta annotations)。@Mapping注解现在支持ElementType#ANNOTATION_TYPE,除了ElementType#METHOD之外。这使得@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 {
}
可以用于表征实体(Entity),而无需具有公共基类型。例如,在下面的StorageMapper中,ShelveEntity和BoxEntity没有共同的基类型。
@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假设目标Bean ShelveEntity和BoxEntity都具有属性:“id”,“creationDate"和"name”。它还假设源Bean ShelveDto和BoxDto始终具有属性"groupName"。这个概念也被称为"鸭子类型"(duck-typing)。换句话说,如果它像鸭子一样嘎嘎叫,走路像鸭子,那它可能是只鸭子。
这个功能仍然处于实验阶段。错误消息还不够成熟:显示出发生问题的方法,以及@Mapping注解中涉及的值。然而,组合的方面并不可见。消息"好像"是@Mapping直接出现在相关方法上。因此,用户在使用此功能时应小心,特别是在不确定属性是否始终存在时。
一种更类型安全(但也更冗长)的方法是在目标Bean和源Bean上定义基类/接口,并使用@InheritConfiguration来实现相同的结果(参见映射配置继承)。
3.3 向映射器添加自定义方法
在某些情况下,可能需要手动实现从一种类型到另一种类型的特定映射,这种映射无法由MapStruct生成。处理此情况的一种方式是在另一个类上实现自定义方法,然后由MapStruct生成的映射器使用该方法(参见调用其他映射器)。
或者,当使用Java 8或更高版本时,您可以直接在映射器接口中实现自定义方法作为默认方法。生成的代码将在参数和返回类型匹配时调用默认方法。
例如,假设从Person到PersonDto的映射需要一些无法由MapStruct生成的特殊逻辑。您可以像下面的示例一样定义上一个示例中的映射器:
@Mapper
public interface CarMapper {
@Mapping(...)
...
CarDto carToCarDto(Car car);
default PersonDto personToPersonDto(Person person) {
//手动编写的映射逻辑
}
}
由MapStruct
生成的类实现了carToCarDto()
方法。在carToCarDto()方法中生成的代码将在映射driver属性
时调用手动实现的personToPersonDto()
方法。
映射器还可以以抽象类的形式定义,而不是接口,并直接在映射器类中实现自定义方法。在这种情况下,MapStruct将生成一个扩展该抽象类的类,并实现所有抽象方法。与声明默认方法相比,这种方法的优点是可以在映射器类中声明额外的字段。
前面的示例中,从Person到PersonDto的映射需要一些特殊逻辑,可以像下面这样定义:
示例2. 由抽象类定义的映射器
@Mapper
public abstract class CarMapper {
@Mapping(...)
...
public abstract CarDto carToCarDto(Car car);
public PersonDto personToPersonDto(Person person) {
//手动编写的映射逻辑
}
}
MapStruct
将生成一个CarMapper
的子类,并实现carToCarDto()
方法,因为它被声明为抽象方法。在生成的carToCarDto()
方法中,当映射driver属性时,会调用手动实现的personToPersonDto()
方法。
3.4 映射方法具有多个源参数
MapStruct还支持具有多个源参数的映射方法。这在将多个实体组合成一个数据传输对象时非常有用。
示例3. 具有多个源参数的映射方法
@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还提供了直接引用源参数的可能性。
示例4. 直接引用源参数的映射方法
@Mapper
public interface AddressMapper {
@Mapping(target = "description", source = "person.description")
@Mapping(target = "houseNumber", source = "hn")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Integer hn);
}
在这种情况下,源参数直接映射到目标对象,就像上面的示例中演示的那样。非bean类型的参数hn(在这种情况下是java.lang.Integer)被映射到houseNumber属性。
3.5 将嵌套的Bean属性映射到当前目标
如果您不想显式命名嵌套源Bean的所有属性,可以使用"."作为目标。这将告诉MapStruct将源Bean的每个属性映射到目标对象。
示例5. 使用"目标当前"注解"."
@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”)解决了这个冲突。 - @Mapping(target = “.”, source = “record”) 的意思是将 CustomerDto 对象的record 属性的所有属性映射到Customer 对象本身的属性中,而不是将其映射到 Customer 对象的一个名为 record的属性上。
3.6 更新现有的Bean实例
在某些情况下,您可能需要进行映射操作,而不是创建目标类型的新实例,而是更新现有的实例。这种类型的映射可以通过添加一个目标对象参数,并使用@MappingTarget注解标记该参数来实现。
示例6. 更新方法
@Mapper
public interface CarMapper {
void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}
- updateCarFromDto方法的生成代码将使用CarDto对象的属性值更新传入的Car实例。只能有一个参数被标记为映射的目标对象。您还可以将方法的返回类型设置为目标参数的类型,这将导致生成的实现在更新传入的映射目标对象后将其返回,从而实现流畅的映射方法调用。
- 对于集合或映射类型的属性,根据不同的CollectionMappingStrategy策略,目标对象的属性更新行为有所不同:
- 对于CollectionMappingStrategy.ACCESSOR_ONLY策略,目标对象中的集合或映射类型属性将被清空,然后用相应源集合或映射的值进行填充。
- 对于CollectionMappingStrategy.ADDER_PREFERRED或CollectionMappingStrategy.TARGET_IMMUTABLE策略,目标对象不会被清空,值将立即进行填充。
通过更新现有的目标对象而不是创建新实例,您可以在保留对象状态的同时更新特定属性,这在许多场景下非常有用。这样,您可以灵活地处理目标对象的更新,并在需要时返回更新后的对象。
3.7 使用直接字段访问的映射
1、MapStruct还支持映射具有没有getter/setter的公共字段。如果MapStruct找不到适合属性的getter/setter方法,它将使用这些字段作为读取/写入访问器。
2、如果字段是公共的或公共final的,则视为读取访问器。如果字段是静态的,则不被视为读取访问器。
3、只有当字段是公共的时,才被视为写入访问器。如果字段是final和/或静态的,则不被视为写入访问器。
示例7. 映射的示例类
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);
}
在上述示例中,Source和Destination类的属性都是直接的公共字段。MapStruct将使用这些字段进行读取和写入操作来完成属性的映射。
对于上述配置,生成的映射器如下所示:
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();
// ...
}
}
3.8 使用构建器
MapStruct还支持通过构建器对不可变类型进行映射。在进行映射时,MapStruct会检查是否存在适用于被映射类型的构建器。这是通过BuilderProvider SPI完成的。如果存在特定类型的构建器,则该构建器将用于映射过程。
BuilderProvider的默认实现假设以下条件:
-
类型具有无参数的公共静态构建器创建方法,该方法返回一个构建器。例如,Person具有一个返回PersonBuilder的公共静态方法。
-
构建器类型具有无参数的公共方法(构建方法),该方法返回正在构建的类型。在我们的例子中,PersonBuilder具有返回Person的方法。
-
如果存在多个构建方法,MapStruct将寻找名为build的方法,如果存在该方法,则使用该方法,否则将生成一个编译错误。
-
可以使用@Builder在@BeanMapping、@Mapper或@MapperConfig中定义特定的构建方法。
-
如果存在满足上述条件的多个构建器创建方法,则DefaultBuilderProvider
SPI将引发MoreThanOneBuilderCreationMethodException。在MoreThanOneBuilderCreationMethodException情况下,MapStruct将在编译中输出警告并不使用任何构建器。 -
如果找到这样的类型,则MapStruct将使用该类型执行映射(即它将在该类型中查找setter方法)。为了完成映射,MapStruct生成的代码将调用构建器的build方法。
-
可以通过@Builder#disableBuilder关闭构建器检测。如果禁用了构建器,MapStruct将退回到常规的getter/setter方法。
-
构建器类型也会考虑对象工厂。例如,如果我们的PersonBuilder存在对象工厂,则该工厂将替代构建器创建方法。
-
检测到的构建器会影响@BeforeMapping和@AfterMapping的行为。
示例8. 使用构建器的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 );
}
}
}
示例9. Person映射器定义
public interface PersonMapper {
Person map(PersonDto dto);
}
示例10. 使用生成器生成映射器
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.8 使用构造函数
MapStruct支持使用构造函数进行目标类型的映射。在进行映射时,MapStruct会检查是否存在与正在映射的类型对应的构建器。如果没有构建器,MapStruct将查找一个可访问的单个构造函数。
当存在多个构造函数时,会按照以下方式选择要使用的构造函数:
- 如果某个构造函数上标有名为@Default的注解(来自任何包,参见非内置注解),则该构造函数将被使用。
- 如果存在单个公共构造函数,则将使用该构造函数来构造对象,并忽略其他非公共构造函数。
- 如果存在无参数构造函数,则将使用该构造函数来构造对象,并忽略其他构造函数。
- 如果存在多个符合条件的构造函数,则由于存在歧义性而导致编译错误。为了消除歧义,可以使用名为@Default的注解(来自任何包,参见非内置注解)。
示例11. 选择要使用的构造函数
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无法选择构造函数
public Van(String make) {
}
public Van(String make, String color) {
}
}
当使用构造函数时,构造函数的参数名称将用作目标属性的名称并进行匹配。如果构造函数上存在名为@ConstructorProperties的注解(来自任何包,参见非内置注解),则将使用该注解获取参数的名称。
当存在对象工厂方法或使用@ObjectFactory注解的方法时,它将优先于目标中定义的任何构造函数。在这种情况下,将不使用目标对象构造函数。
示例12. 带有构造函数参数的Person类
public class Person {
private final String name;
private final String surname;
public Person(String name, String surname) {
this.name = name;
this.surname = surname;
}
}
示例13. 带有构造函数的PersonMapper定义
public interface PersonMapper {
Person map(PersonDto dto);
}
示例14. 带有构造函数的生成的Mapper
// 生成的代码
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 提供了一种透明的方式来执行这种映射,通过使用目标 bean 的属性(或通过 Mapping#source 定义)从 map 中提取值。
示例15. 映射 map 到 bean 的示例类
public class Customer {
private Long id;
private String name;
// 省略 getters 和 setters
}
@Mapper
public interface CustomerMapper {
@Mapping(target = "name", source = "customerName")
Customer toCustomer(Map<String, String> map);
}
示例16. 成的映射器,用于将 map 映射到 bean
// 生成的代码
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<String, Integer> 映射到 bean,其中对于每个属性,需要从 Integer 进行相应的属性转换。
当使用原始 map 或不以 String 作为键的 map 时,将生成警告。如果 map 本身直接映射到其他目标属性,则不会生成警告。