MapStruct之数据类型转换(下)

1,457 阅读5分钟

调用自定义映射方法

场景:某些属性需要自定义映射逻辑时, 示例如下:
源对象:

@Getter
@Setter
public class GoodsDTO {
    private String code;
    private String count;
    private String price;
}

目标对象:

@Getter
@Setter
public class Goods {
    private String code;
    private int count;
    private float price;
}

映射接口:

@Mapper(componentModel = "spring")
public interface GoodsConverter {
    @Mapping(target = "code", source = "goods")
    GoodsDTO convert2(Goods goods);

    /**
     * 自定义映射方法
     * @param goods
     * @return
     */
    default String convertCode(Goods goods) {
        return "customer-" + goods.getCode();
    }
}

在映射方法convert2中对code字段映射时会调用自定义的convertCode方法。其中@Mapping(target = "code", source = "goods")source = "goods"表示自定义方法中Goods类型参数名。该示例中自定义映射方法与前面介绍的映射器添加自定义方法有所不同,前者自定义方法中入参类型与源对象类型一致并且配合@Mapping使用(指定某个属性映射时调用),后者自定义方法中入参类型与源对象属性类型一致,匹配所有源对象属性类型一致的属性映射(假如自定义了一个String类型的映射方法,源对象中有3个String类型的属性在映射时都会调用这个自定义的方法)。

调用其他映射器

除了在同一映射器接口或抽象类中定义的方法外,MapStruct还可以调用其他类(可以是@Mapper标记的映射接口或自定义类)中定义的映射方法。通过@Mapper中use属性传入类可以实现调用其他映射器。这样可以简写代码。
例如,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属性。
注意:生成的映射器会使用为其配置的组件模型来检索引用的映射器。例如,如果spring被用作CarMapper的组件模型,则DateMapper也必须是spring托管的bean。当使用默认组件模型时,任何由MapStruct生成的映射器引用的手写映射器类必须声明一个public无参构造函数,以便能够实例化。

将映射目标类型传递给自定义映射方法

可以在自定义映射方法中定义一个额外的参数,类型为Class(或其超类型),该参数必须使用@TargetType标记,以便MapStruct生成调用,传递表示目标bean的相应属性类型的Class实例。

示例一

User中name和nickeName属性都为string类型将调用下自定义的方法


UserDTO convert(User user);

default  <T> T resolve(String reference, @TargetType Class<T> entityClass) {
    // do something
    return null;
}

生成的代码:

@Override
public UserDTO convert(User user) {
    if ( user == null ) {
        return null;
    }

    UserDTO userDTO = new UserDTO();

    userDTO.setId( user.getId() );
    userDTO.setName( resolve( user.getName(), String.class ) );
    userDTO.setNickName( resolve( user.getNickName(), String.class ) );
    userDTO.setAge( user.getAge() );

    return userDTO;
}

示例二

CarDto可以具有类型为Reference的属性owner,其中包含Person实体的主键。现在可以创建一个通用的自定义映射器,将任何Reference对象解析为相应的受管理的JPA实体实例。
通用映射器

@ApplicationScoped // CDI component model
public class ReferenceMapper {

    @PersistenceContext
    private EntityManager entityManager;

    public <T extends BaseEntity> T resolve(Reference reference, @TargetType Class<T> entityClass) {
        return reference != null ? entityManager.find( entityClass, reference.getPk() ) : null;
    }

    public Reference toReference(BaseEntity entity) {
        return entity != null ? new Reference( entity.getPk() ) : null;
    }
}

引用通用映射器

@Mapper(componentModel = "cdi", uses = ReferenceMapper.class )
public interface CarMapper {

    Car carDtoToCar(CarDto carDto);
}

生成的代码

@ApplicationScoped
public class CarMapperImpl implements CarMapper {

    @Inject
    private ReferenceMapper referenceMapper;

    @Override
    public Car carDtoToCar(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }

        Car car = new Car();

        car.setOwner( referenceMapper.resolve( carDto.getOwner(), Owner.class ) );
        // ...

        return car;
    }
}

将上下文或状态对象传递给自定义方法

生成的映射方法可以通过@Context参数传递额外的上下文或状态信息到自定义方法中。这些参数会传递给其他映射方法、@ObjectFactory方法(参见对象工厂)或@BeforeMapping / @AfterMapping方法(参见使用before-mapping和after-mapping方法进行映射自定义)(如果适用),因此可以在自定义代码中使用。
@Context参数会搜索@ObjectFactory方法,如果适用,则在提供的上下文参数值上调用它们。
@Context参数还会搜索@BeforeMapping / @AfterMapping方法,如果适用,则在提供的上下文参数值上调用它们。
注意:在调用上下文参数上的before/after映射方法之前不会执行空值检查。在这种情况下,调用方需要确保不传递null。
为了让生成的代码调用使用@Context参数声明的方法,生成的映射方法的声明需要至少包含这些(或可分配的)@Context参数。生成的代码不会创建缺少的@Context参数的新实例,也不会传递字面上的null。

使用@Context参数将数据传递给自定义的映射方法:

public abstract CarDto toCar(Car car, @Context Locale translationLocale);

protected OwnerManualDto translateOwnerManual(OwnerManual ownerManual, @Context Locale locale) {
    // manually implemented logic to translate the OwnerManual with the given Locale
}

生成的代码:

//GENERATED CODE
public CarDto toCar(Car car, Locale translationLocale) {
    if ( car == null ) {
        return null;
    }

    CarDto carDto = new CarDto();

    carDto.setOwnerManual( translateOwnerManual( car.getOwnerManual(), translationLocale );
    // more generated mapping code

    return carDto;
}

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

场景:有时候需要定义具有相同方法签名(除名称外,入参与出参相同)但具有不同行为的映射方法,编译的时候MapStruct生成代码时不知道调用哪个方法导致编译出错。这时候可以借助@Named 这个注解对方法进行标记,然后再@Mapping中借助属性qualifiedByName告诉MapStruct 调用哪个方法。也可以结合@Qualifier 自定义注解实现方法选择。推荐前面@Named 方式实现更简单。

下面是通过@Name 注解实现映射方法选择:

通用映射类:

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

}