MapStruct之数据类型转换(上)

2,245 阅读4分钟

映射的属性时源对象和目标对象不一定总是相同的类型。比如一个属性在源对象中可能是int类型,但在目标对象中是Long类型。Car类可能有一个Person类型的属性,在映射Car对象时需要将其转换为PersonDto类型。
这里将介绍MapStruct如何处理上述这种情况的类型转换。

隐式类型转换

在许多情况下,MapStruct会自动处理类型转换。比如int类型与String类型,生成的代码将通过调用String#valueOf(int) 和Integer#parseInt(String)来隐式地进行转换。
目前将自动映射以下转换:

  • 在所有基本类型与其包装类型之间,比如int与Integer、boolean与Boolean等,当将基本类型转成基础类型时会进行null判断。
  • 在所有基本类型与包装类型之间,比如int和龙,或者byte与Integer之间。从较大的数据类型转换为较小的数据类型(例如从long转换为int)可能会导致值精度损失。Mapper和MapperConfig注解有一个属性typeConversionPolicy控制用于控制警告/错误。由于向后兼容性的原因,默认为ReportingPolicy.IGNORE
  • 在所有Java基本类型(包括它们的包装类)和String类型之间,例如int和String或Boolean和String之间,可以指定类似于java.text.DecimalFormat所理解的格式字符串。

示例:
GoodsDTO:

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

Goods:

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

映射方法:

@Mapper(componentModel = "spring")
public interface GoodsConverter {
    @Mapping(target = "count", source = "count", numberFormat = "$#.00")
    @Mapping(target = "price",source = "price", numberFormat = "#.###")
    GoodsDTO convert(Goods goods);
}

生成的代码:

@Component
public class GoodsConverterImpl implements GoodsConverter {

    @Override
    public GoodsDTO convert(Goods goods) {
        if ( goods == null ) {
            return null;
        }

        GoodsDTO goodsDTO = new GoodsDTO();

        goodsDTO.setCount( new DecimalFormat( "$#.00" ).format( goods.getCount() ) );
        goodsDTO.setPrice( new DecimalFormat( "#.###" ).format( goods.getPrice() ) );
        goodsDTO.setCode( goods.getCode() );

        return goodsDTO;
    }
}
  • 枚举类型与String之间
  • 可以在大数字类型(java.math.BigInteger、java.math.BigDecimal)和Java基本类型(包括其包装类)以及字符串之间进行转换。可以指定由java.text.DecimalFormat理解的格式字符串。
  • 常见时间格式之间转换
  • 常见时间类型Date,LocalDate,LocalDateTime,LocalTime,ZonedDateTime 等与String之间转换可以通过@Mapping的属性dateFormat指定格式。
@Mapper
public interface CarMapper {

    @Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")
    CarDto carToCarDto(Car car);

    @IterableMapping(dateFormat = "dd.MM.yyyy")
    List<String> stringListToDateList(List<Date> dates);
}

映射对象引用

场景:源对象中存在属性引用类型与目标对象属性的引用类型不一致的情况。如下例子:
源对象:

@Getter
@Setter
public class FamilyDetail {
    UserDetailDTO userDetail;
    Address address;
}

目标对象:

@Getter
@Setter
public class FamilyDTO {
    private User user;

    private Address address;
}

映射方法:

@Mapping(target = "user", source = "userDetail")
FamilyDTO convertFamily(FamilyDetail detail);

User convertUser(UserDetailDTO dto);

对于这种情况,MapStruct将针对源对象和目标对象中的每个属性对执行以下过程:

  • 如果源属性和目标属性具有相同的类型,则直接从源复制值到目标。如果属性是一个集合(例如List),则会将集合的副本设置到目标属性中。
  • 如果源属性和目标属性类型不同,则检查是否存在另一个映射方法,其参数类型为源属性的类型,返回类型为目标属性的类型。如果存在这样的方法,则将在生成的映射实现中调用它。可以参考前面的映射器添加到自定义方法中的应用。
  • 如果不存在这样的方法,则MapStruct将查找是否存在源和目标属性类型的内置转换。如果存在,则生成的映射代码将应用此转换。如上面例子,在生成convertFamily的代码中将会调用convertUse方法r。
  • 如果不存在这样的方法,则MapStruct将应用复杂转换:

映射方法,映射方法映射的结果,例如:target = method1(method2(source))
内置转换,映射方法映射的结果,例如:target = method(conversion(source))
映射方法,内置转换映射的结果,例如:target = conversion(method(source))

  • 如果没有找到这样的方法,则MapStruct将尝试生成自动子映射方法,该方法将在源属性和目标属性之间进行映射。如果想禁用自动生成子映射方法可以使用@Mapper(disableSubMappingMethodsGeneration=true)。
  • 如果MapStruct无法创建基于名称的映射方法,则会在构建时引发错误,指示无法映射的属性及其路径。

控制嵌套对象映射

场景:源对象和目标对象属性名不一样时或者嵌套后的属性不一样时可以用@Mapping中target和source通过.指定映射关系。如下:

@Mapper
public interface FishTankMapper {

    @Mapping(target = "fish.kind", source = "fish.type")
    @Mapping(target = "fish.name", ignore = true)
    @Mapping(target = "ornament", source = "interior.ornament")
    @Mapping(target = "material.materialType", source = "material")
    @Mapping(target = "quality.document.organisation.name", constant = "NoIdeaInc" )
    FishTankDto map( FishTank source );
}

在上述例子中 @Mapping(target = "quality.document.organisation.name", constant = "NoIdeaInc" ),还可以给目标属性指定常量值。生成的代码中quality.document.organisation.name中的值为设定的NoIdeaInc。