MapStruct中文文档(一)——定义映射器

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);
}

  1. @Mapper注解会导致MapStruct代码生成器在构建时创建CarMapper接口的实现。
  2. 在生成的方法实现中,源类型(例如Car)的所有可读属性将被复制到目标类型(例如CarDto)的相应属性中:
  3. 当属性与目标实体的属性同名时,它们将被隐式映射。 当属性在目标实体中具有不同的名称时,可以通过@Mapping注解指定其名称。
  4. 必须在@Mapping注解中指定属性名称,该名称定义在JavaBeans规范中,例如对于具有访问器方法getSeatCount()和setSeatCount()的属性,名称为seatCount。
  5. 通过@BeanMapping(ignoreByDefault =
    true),默认行为将是显式映射,意味着所有映射都必须通过@Mapping指定,并且在缺少目标属性时不会发出警告。这允许忽略所有字段,除了通过@Mapping显式定义的字段。
  6. 还支持流畅的设置器(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);
}

上述的映射方法接受两个源参数,并返回一个组合后的目标对象。与单参数映射方法一样,属性通过名称进行映射。

  1. 如果多个源对象都使用相同的属性名称进行定义,必须使用@Mapping注解来指定从哪个源参数获取属性,就像示例中的description属性一样。如果不解决这种歧义,将引发错误。对于在给定的源对象中只存在一次的属性,可以选择性地指定源参数的名称,因为它可以自动确定。
  2. 在使用@Mapping注解时,指定属性所在的参数是强制的。
  3. 具有多个源参数的映射方法在所有源参数都为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);
}
  1. 生成的代码将直接CustomerDto.record的每个属性映射到Customer,无需手动命名它们。对于Customer.account也是同样的情况。
  2. 当存在冲突时,可以通过显式定义映射来解决。例如在上面的示例中,name出现在CustomerDto.record和CustomerDto.account中。映射@Mapping(target
    = “name”, source = “record.name”)解决了这个冲突。
  3. @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);
}
  1. updateCarFromDto方法的生成代码将使用CarDto对象的属性值更新传入的Car实例。只能有一个参数被标记为映射的目标对象。您还可以将方法的返回类型设置为目标参数的类型,这将导致生成的实现在更新传入的映射目标对象后将其返回,从而实现流畅的映射方法调用。
  2. 对于集合或映射类型的属性,根据不同的CollectionMappingStrategy策略,目标对象的属性更新行为有所不同:
  3. 对于CollectionMappingStrategy.ACCESSOR_ONLY策略,目标对象中的集合或映射类型属性将被清空,然后用相应源集合或映射的值进行填充。
  4. 对于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的默认实现假设以下条件:

  1. 类型具有无参数的公共静态构建器创建方法,该方法返回一个构建器。例如,Person具有一个返回PersonBuilder的公共静态方法。

  2. 构建器类型具有无参数的公共方法(构建方法),该方法返回正在构建的类型。在我们的例子中,PersonBuilder具有返回Person的方法。

  3. 如果存在多个构建方法,MapStruct将寻找名为build的方法,如果存在该方法,则使用该方法,否则将生成一个编译错误。

  4. 可以使用@Builder在@BeanMapping、@Mapper或@MapperConfig中定义特定的构建方法。

  5. 如果存在满足上述条件的多个构建器创建方法,则DefaultBuilderProvider
    SPI将引发MoreThanOneBuilderCreationMethodException。在MoreThanOneBuilderCreationMethodException情况下,MapStruct将在编译中输出警告并不使用任何构建器。

  6. 如果找到这样的类型,则MapStruct将使用该类型执行映射(即它将在该类型中查找setter方法)。为了完成映射,MapStruct生成的代码将调用构建器的build方法。

  7. 可以通过@Builder#disableBuilder关闭构建器检测。如果禁用了构建器,MapStruct将退回到常规的getter/setter方法。

  8. 构建器类型也会考虑对象工厂。例如,如果我们的PersonBuilder存在对象工厂,则该工厂将替代构建器创建方法。

  9. 检测到的构建器会影响@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将查找一个可访问的单个构造函数。

当存在多个构造函数时,会按照以下方式选择要使用的构造函数:

  1. 如果某个构造函数上标有名为@Default的注解(来自任何包,参见非内置注解),则该构造函数将被使用。
  2. 如果存在单个公共构造函数,则将使用该构造函数来构造对象,并忽略其他非公共构造函数。
  3. 如果存在无参数构造函数,则将使用该构造函数来构造对象,并忽略其他构造函数。
  4. 如果存在多个符合条件的构造函数,则由于存在歧义性而导致编译错误。为了消除歧义,可以使用名为@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 本身直接映射到其他目标属性,则不会生成警告。

猜你喜欢

转载自blog.csdn.net/hurtseverywhere/article/details/131701925