文章目录
前言
源对象和目标对象中的映射属性并不总是具有相同的类型。例如,源属性可能是int,但目标 bean 中的类型为Long。
MapStruct 如何处理此类数据类型转换的呢?
1. 隐式类型转换
一般情况下,MapStruct 会自动处理类型转换。例如,如果源对象中一个属性类型为int,但在目标对象中属于String类型,则生成的代码将分别通过调用String#valueOf(int)
和Integer#parseInt(String)
执行转换。
目前支持以下类型自动转换:
转换类型 | 说明 |
---|---|
所有Java基本数据类型及其相应的包装类型 | 例如nt和Integer,boolean和Boolean之间等 |
所有 Java 基本数据类型和包装器类型之间 | 例如在int和long或byte和Integer之间。从较大的数据类型转换为较小的数据类型(例如 long转 int)可能会导致值或精度损失。在Mapper和MapperConfig注解有一个typeConversionPolicy 来控制警告/错误。默认值为“ReportingPolicy.IGNORE”。 |
所有 Java 基本数据(包括包装类)和String之间 | 例如在int和String或Boolean和String之间。java.text.DecimalFormat 可以指定响应格式字符串。 |
在enum类型和String之间。 | |
在大数字类型(java.math.BigInteger, java.math.BigDecimal)和 Java 基本类型(包括包装类)以及字符串之间。 | java.text.DecimalFormat 可以指定响应格式字符串。 |
JAXBElement和T之间,List<JAXBElement>和List | |
java.util.Calendar/java.util.Date和 JAXB之间XMLGregorianCalendar | |
在java.util.Date/XMLGregorianCalendar和之间String | java.text.SimpleDateFormat可以通过选项指定格式字符串 |
时间类转换 | 比如 java.sql.Timestamp和java.util.Date、java.time.LocalDateTime和javax.xml.datatype.XMLGregorianCalendar等等 |
java.util.Currency和String | 该值必须是有效的ISO-4217字母代码,否则会抛出IllegalArgumentException |
java.util.UUID和String | 该值必须是有效的UUID,否则会抛出IllegalArgumentException |
String和StringBuilder | |
java.net.URL和String |
案例演示:
- 创建源及目标对象,其中的字段类型不一致
@Data
public class Person {
String name;
Integer age;
Date jdkDate;
Float money;
}
@Data
@ToString
public class PersonDTO {
String name;
String age;
String strDate;
String money;
}
- 添加转换映射器
@Mapper(componentModel = "spring", typeConversionPolicy = ReportingPolicy.ERROR)
public interface PersonMapper {
// 日志格式化
@Mapping(source = "jdkDate", target = "strDate", dateFormat = "yyyy-MM-dd HH:mm")
// 数字格式化,数字转字符串时才会生效
@Mapping(source = "money", target = "money", numberFormat = "0.00")
PersonDTO person2PersonDTO(Person person);
}
- 查看结果,按照预期进行了转换
2. 映射引用类型
通常,对象不仅具有基础数据类型,而且还有引用类型。例如,Car类可以包含对Person对象(代表汽车的司机),Person对象应该映射到类CarDto中PersonDto对象。
在这种情况下,需要映射属性为引用对象时,只需为引用的对象类型定义一个映射方法即可。
案例演示:
- Car及CarDTO添加Person及PersonDTO属性
@Data
public class Car {
public String make;
public Integer numberOfSeats;
public String type;
public Driver driver;
public Person person;
}
@Data
@ToString
//@Builder(toBuilder=true)
public class CarDto {
public String name;
public String make;
public Integer seatCount;
public String type;
public String driverName;
public PersonDTO personDTO;
}
- 映射器添加方法
@Mapping(source = "person", target = "personDTO")
CarDto car2CarDto(Car car);
PersonDTO person2PersonDTO(Person person);
- 查看编译后的文件,可以看出MapStruct调用了在进行引用对象属性进行转换时,调用了对应的引用对象映射方法。
在生成映射器的实现类方法时,MapStruct 将为源对象和目标对象中的每个属性对应以下规则:
-
如果源和目标属性具有相同的类型,则该值将直接从源复制到目标。如果该属性是一个集合(例如 List),则该集合的副本将被设置到目标属性中。
-
如果源属性类型和目标属性类型不同,检查是否存在其他映射方法,其参数类型为源属性类型,返回类型为目标属性类型。如果存在这样的方法,它将在生成的映射实现中调用。
-
如果不存在这样的方法,MapStruct 将查看属性的源和目标类型的内置转换是否存在。如果是这种情况,生成的映射代码将应用此转换。
-
如果不存在这样的方法,MapStruct 将应用复杂的转换:
- 映射方法,映射方法映射的结果,像这样:target = method1( method2( source ) )
- 内置转换,通过映射方法映射的结果,如下所示:target = method( conversion( source ) )
- 映射方法,内置转换映射的结果,如下所示:target = conversion( method( source ) )
-
如果没有找到这样的方法,MapStruct 将尝试生成一个自动子映射方法,该方法将在源属性和目标属性之间进行映射。
-
如果 MapStruct 无法创建基于名称的映射方法,则会在构建时引发错误,指示不可映射的属性及其路径。
3. 嵌套映射
MapStruct 将根据源和目标属性的名称生成一个方法。不幸的是,在许多情况下,这些名称并不匹配。使用@Mapping注解可以解决这种问题,也可以解决嵌套对象字段映射问题。
@Mapper
public interface FishTankMapper {
// 将源fish对象的type映射给目标对象fish属性的kind
@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.report.organisation.name", source = "quality.report.organisationName")
FishTankDto map( FishTank source );
}
4. 调用自定义映射方法
有时映射并不简单,有些字段需要自定义逻辑。
案例演示:比如数据库账户余额采用分单位,实际用户查看应该显示多少元,这个时候就可以在Mapper 中,自定义该字段的处理逻辑。
@Mapper
public abstract class CustomerMapper {
public static final CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);
@InheritInverseConfiguration
CustomerDto fromCustomer(Customer customer) {
CustomerDto customerDto = new CustomerDto();
customerDto.setMoney(customer.getMoney() / 100);
return customerDto;
}
}
5. 调用其他映射器
除了在同一映射器上定义的方法之外,MapStruct 还可以调用其他类中定义的映射方法。
案例演示:定义一个公共转换器,对时间和字符串转换进行格式化,其他转换器调用该转换器。
创建一个手动映射器类:
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 );
}
}
}
在接口的@Mapper注解中引用了DateMapper,在进行映射时,MapStruct或查找DateMapper中关于时间映射的相关方法进行转换。
@Mapper(uses = DateMapper.class)
public abstract class CustomerMapper {
public static final CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);
@Mapping(target = "name", source = "customerName")
abstract Customer toCustomer(CustomerDto customerDto);
abstract CustomerDto fromCustomer(Customer customer);
@Mapping(target = "name", source = "customerName")
abstract Customer toCustomer(Map<String, String> map);
}
查看生成的代码,可以看到调用:
6. 将映射目标类型传递给自定义映射器
当使用@Mapper#uses()
引入自定义映射器时,在自定义映射器中可以定义类型为Class
(或其超类型)的附加参数,以便对特定目标对象类型执行常规映射任务。必须使用@TargetType
注解标识该参数。
案例演示:
@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);
}
然后 MapStruct 将生成如下内容:
//GENERATED CODE
@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;
}
}
7. 将上下文或状态对象传递给自定义方法
额外的上下文或状态信息可以通过带有@Context
注解的参数传递到自定义方法。此类参数会传递给其他映射方法、@ObjectFactory
方法、@BeforeMapping @AfterMapping
方法。
-
@Context搜索参数以查找@ObjectFactory方法,如果适用,在提供的上下文参数值上调用这些方法。
-
@Context参数也会搜索@BeforeMapping/@AfterMapping方法,如果适用,这些方法会在提供的上下文参数值上调用。
注意:null在对上下文参数调用 before/after 映射方法之前不执行任何检查。调用者需要确保null在这种情况下不会传递。
生成的代码要调用带有@Context
参数声明的方法,生成的映射方法的声明也至少需要包含那些(或可分配的)@Contex
t参数。生成的代码不会创建缺少@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
}
然后 MapStruct 将生成如下内容:
//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;
}
8. 映射方法解析
将属性从一种类型映射到另一种类型时,MapStruct 会查找将源类型映射到目标类型的最具体方法。该方法可以在同一个映射器接口上声明,也可以在通过@Mapper#uses()
引入其他映射器, 这同样适用于工厂方法(请参阅对象工厂)。
查找映射或工厂方法的算法尽可能类似于 Java 的方法解析算法。特别是,具有更具体源类型的方法将优先(例如,如果有两种方法,一种映射搜索的源类型,另一种映射相同的超类型)。如果找到不止一种最具体的方法,则会引发错误。
使用 JAXB 时,例如将String转换为相应的 JAXBElement<String>
时,MapStruct 将在查找映射方法时考虑@XmlElementDecl
注解的scope和name属性。这可确保创建的JAXBElement实例具有正确的值。
9. 基于限定符的映射方法选择
在许多情况下,需要映射具有相同方法参数(名称除外)但具有不同行为的方法。比如在隐射器中,添加对某个字段转换时候需要翻译成中文或英文。
public String translateTitleEG(String title) {
// some mapping logic
return "英文";
}
public String translateTitleChinese(String title) {
// some mapping logic
return "中文";
}
这个时候编译,会报错:
如果不使用限定符,这将导致不明确的映射方法错误,因为找到了 2 个限定方法 ( translateTitleEG
, translateTitleChinese
) 并且 MapStruct 不会提示选择哪一个。
MapStruct提供了@Qualifier(org.mapstruct.Qualifier)
注解来解决这个问题。
首先我们使用@Qualifier
创建三个注解:
@Qualifier
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface TitleTranslator {
}
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface ChineseTitle {
}
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface EnglishTitle {
}
在Mapper 中添加上面的注解,方法上使用不同的注解标识:
@TitleTranslator
public class BaseMapper {
@EnglishTitle
public String translateTitleEG(String title) {
// some mapping logic
return "英文";
}
@ChineseTitle
public String translateTitleChinese(String title) {
// some mapping logic
return "中文";
}
}
在映射器转换对象的方法中,使用qualifiedBy
选择使用哪个注解标识的方法来处理该字段:
@Mapping( target = "title", qualifiedBy = { TitleTranslator.class, ChineseTitle.class } )
CarDto car2CarDto(Car car);
测试发现,可以正常使用我们指定的方法进行转换:
MapStruct 还提供了@Named
注解,可以使用更简单的方式来进行限定使用,或者更直接地通过它的值来命名一个映射方法。上面的相同示例如下所示:
@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 );
}
10. 将限定符与默认值相结合
@Mapping
注解的defaultValue
属性,可以指定一个字符串类型的默认值。Mapping#qualifiedByName
、Mapping#qualifiedBy
强制MapStruct 使用其指定的方法。
可以结合defaultValue
和qualifiedBy
属性,放入参的值为null 时,defaultValue
默认值将被传递给qualifiedBy
指定的方法。
比如以下代码中:
@Mapping( target = "title", qualifiedBy = { TitleTranslator.class, ChineseTitle.class } ,defaultValue = "defalut")
CarDto car2CarDto(Car car);
如果title
字段为null,将会调用translateTitleChinese
方法,入参为defalut
@ChineseTitle
public String translateTitleChinese(String title) {
// some mapping logic
System.out.println(title);
return "中文";
}