一、MapStruct是什么

1.1 使用背景

  1. 需求场景多。
    在我们日常的Java开发工作中,经常会遇到模型对象的转化,例如从实体类转化为DTO模型,DTO转化为VO、TO等模型的业务场景
  2. 模型转化枯燥、编码价值低。而模型的转化是枯燥且无技术含量的,不仅耗费大量时间而且很容易出错
  3. 问题排查困难。若涉及多个具有很多字段的Bean的模型转化时,不得不排查是否两个模型的相同字段的转化映射关系有缺失

1.2 优势

  1. MapStruct 是一个 Java注释处理器,用于生成类型安全的 bean 映射类。
  2. 您所要做的就是定义一个Bean的映射抽象类,在该抽象类中声明任何所需的映射方法。
  3. 在编译期间,MapStruct 将生成此抽象类的实现类。这个实现使用普通的 Java 方法调用来映射源对象和目标对象。
  4. 与手动编写映射代码相比,MapStruct 通过生成编写繁琐且容易出错的代码来节省时间。
  5. 与动态映射框架相比,MapStruct 具有以下优势:
    (1)通过使用普通方法getter、setter调用,而不是反射来快速执行,效率很高。
    (2)编译时类型安全:只能映射相互映射的对象和属性,不会将其余模型属性进行映射

二、基础用法详解

2.1 定义映射器

2.1.1 基本映射

要创建映射器,只需使用所需的映射方法定义一个 Java 接口并使用注释对其进行org.mapstruct.Mapper注释:

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

MapStruct 的一般理念是生成的代码看起来尽可能像您自己亲手编写的代码。特别是这意味着通过简单的 getter/setter 调用而不是反射或类似方法将值从源复制到目标。

2.1.2 具有多个源参数的映射方法

MapStruct 还支持具有多个源参数的映射方法。这很有用,例如,为了将多个实体组合成一个数据传输对象。

@Mapper
public interface AddressMapper {
 
    @Mapping(target = "description", source = "person.description")
    @Mapping(target = "houseNumber", source = "address.houseNo")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}

显示的映射方法采用两个源参数并返回一个组合的目标对象。与单参数映射方法一样,属性按名称映射。

如果多个源对象定义了具有相同名称的属性,则必须使用@Mapping注释指定从中检索属性的源参数,如示例中的description属性所示。当这种歧义未解决时,将引发错误。对于在给定源对象中只存在一次的属性,可以选择指定源参数的名称,因为它可以自动确定。

2.1.3 将嵌套的 bean 属性映射到当前目标

如果您不想显式命名嵌套源 bean 中的所有属性,则可以将其.用作目标。这将告诉 MapStruct 将每个属性从源 bean 映射到目标对象。

@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” )解决了这个冲突。

2.1.4 Map到Bean的映射

public class Customer {
 
    private Long id;
    private String name;
 
    //getters and setter omitted for brevity
}
 
@Mapper
public interface CustomerMapper {
 
    @Mapping(target = "name", source = "customerName")
    Customer toCustomer(Map<String, String> map);
 
}
2.1.4.1 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( source.get( "customerName" ) );
        }
        // ...
    }
}

2.2 数据类型转化

源对象和目标对象中的映射属性并不总是具有相同的类型。例如,属性可能int属于源 bean 中的类型Long,但属于目标 bean 中的类型。

另一个例子是对其他对象的引用,这些对象应该映射到目标模型中的相应类型。例如,类Car可能具有在映射对象时需要转换为对象driver的类型的属性。PersonPersonDtoCar

2.2.1 隐式类型转化

在许多情况下,MapStruct 会自动处理类型转换。例如,如果一个属性int在源 bean 中属于类型但String在目标 bean 中属于类型,则生成的代码将分别通过调用String#valueOf(int)和透明地执行转换Integer#parseInt(String)(自动完成转化)
目前自动应用以下转换:

  • 之间的所有Java基本数据类型及其相应的包装类型,例如之间int和Integer,boolean和Boolean等生成的代码是null转换一个包装型成相应的原始类型时一个感知,即,null检查将被执行。
  • 在所有 Java 原始数字类型和包装器类型之间,例如在int和long或byte和之间Integer。
  • 在所有 Java原始类型(包括它们的包装器)和String之间,例如在int和String或Boolean和之间String。java.text.DecimalFormat可以指定由
    理解的格式字符串。
2.1.1.1 从int到String的转换
@Mapper
public interface CarMapper {
 
    @Mapping(source = "price", numberFormat = "$#.00")
    CarDto carToCarDto(Car car);
 
    @IterableMapping(numberFormat = "$#.00")
    List<String> prices(List<Integer> prices);
}
2.1.1.2 从 BigDecimal 到 String 的转换
@Mapper
public interface CarMapper {
 
    @Mapping(source = "power", numberFormat = "#.##E0")
    CarDto carToCarDto(Car car);
 
}
2.1.1.3 从日期到字符串的转换
@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);
}

2.2.2 控制嵌套的 bean 映射

在最简单的情况下,嵌套级别上有一个属性需要更正。以fish在FishTankDto和 中具有相同名称的属性为例FishTank。对于这个属性 MapStruct 自动生成一个映射:FishDto fishToFishDto(Fish fish)。MapStruct 不可能知道有偏差的属性kind和type。因此,这可以在映射规则中解决:@Mapping(target=“fish.kind”, source=“fish.type”). 这告诉 MapStruct 偏离kind在此级别查找名称并将其映射到type.

2.2.2.1 控制嵌套 bean 映射的映射器 I
@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.report.organisation.name", source = "quality.report.organisationName")
    FishTankDto map( FishTank source );
}
  1. 相同的构造可用于在嵌套级别忽略某些属性,如第二条@Mapping规则所示。
  2. 当源和目标不共享相同的嵌套级别(相同数量的属性)时,MapStruct
    甚至可以用于“挑选”属性。这可以在源和目标类型中完成。这在接下来的 2条规则中得到了证明:@Mapping(target=“ornament”,
    source=“interior.ornament”)和@Mapping(target=“material.materialType”,
    source=“material”)。
2.2.2.2 控制嵌套 bean 映射的映射器 II
@Mapper
public interface FishTankMapperWithDocument {
 
    @Mapping(target = "fish.kind", source = "fish.type")
    @Mapping(target = "fish.name", expression = "java(\"Jaws\")")
    @Mapping(target = "plant", ignore = true )
    @Mapping(target = "ornament", ignore = true )
    @Mapping(target = "material", ignore = true)
    @Mapping(target = "quality.document", source = "quality.report")
    @Mapping(target = "quality.document.organisation.name", constant = "NoIdeaInc" )
    FishTankWithNestedDocumentDto map( FishTank source );
 
}

请注意@Mapping(target=“quality.document”, source=“quality.report”). DocumentDto在目标端不存在。它是从Report. MapStruct 继续在这里生成映射代码。该映射本身可以被引导到另一个名称。这甚至适用于常量和表达式。这在最后一个例子中显示:@Mapping(target=“quality.document.organisation.name”, constant=“NoIdeaInc”).

2.3 集合映射

  1. 集合类型(的映射List,Set等等)以相同的方式映射豆类型,即通过定义与在映射器接口所需的源和目标类型的映射方法进行。MapStruct 支持来自Java Collection Framework 的各种可迭代类型。
  2. 生成的代码将包含一个循环,循环遍历源集合,转换每个元素并将其放入目标集合。如果在给定的映射器或其使用的映射器中找到集合元素类型的映射方法,则调用此方法来执行元素转换。或者,如果存在源元素和目标元素类型的隐式转换,则将调用此转换例程。
具有集合映射方法的映射器
@Mapper
public interface CarMapper {
 
    Set<String> integerSetToStringSet(Set<Integer> integers);
 
    List<CarDto> carsToCarDtos(List<Car> cars);
 
    CarDto carToCarDto(Car car);
}

生成的实现为每个元素integerSetToStringSet执行从Integer到到的转换String,而生成carsToCarDtos()的carToCarDto()方法为每个包含的元素调用方法,如下所示:

生成的集合映射方法
@Override
public Set<String> integerSetToStringSet(Set<Integer> integers) {
    if ( integers == null ) {
        return null;
    }
 
    Set<String> set = new LinkedHashSet<String>();
 
    for ( Integer integer : integers ) {
        set.add( String.valueOf( integer ) );
    }
 
    return set;
}
 
@Override
public List<CarDto> carsToCarDtos(List<Car> cars) {
    if ( cars == null ) {
        return null;
    }
 
    List<CarDto> list = new ArrayList<CarDto>();
 
    for ( Car car : cars ) {
        list.add( carToCarDto( car ) );
    }
 
    return list;
}//GENERATED CODE
carDto.setPassengers( personsToPersonDtos( car.getPassengers() ) );
...

在映射 bean 的集合类型属性时,例如从Car#passengers(类型List)到CarDto#passengers(类型List),MapStruct 将寻找具有匹配参数和返回类型的集合映射方法。

使用集合映射方法来映射 bean 属性
//GENERATED CODE
carDto.setPassengers( personsToPersonDtos( car.getPassengers() ) );
...

2.3.1 Map集合映射

public interface SourceTargetMapper {
 
    @MapMapping(valueDateFormat = "dd.MM.yyyy")
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}
2.3.1.1Map映射方法的生成实现
@Override
public Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> source) {
    if ( source == null ) {
        return null;
    }
 
    Map<Long, Date> map = new LinkedHashMap<Long, Date>();
 
    for ( Map.Entry<String, String> entry : source.entrySet() ) {
 
        Long key = Long.parseLong( entry.getKey() );
        Date value;
        try {
            value = new SimpleDateFormat( "dd.MM.yyyy" ).parse( entry.getValue() );
        }
        catch( ParseException e ) {
            throw new RuntimeException( e );
        }
 
        map.put( key, value );
    }
 
    return map;
}

2.4 值映射

2.4.1 将枚举映射到枚举类型

  1. MapStruct 支持生成将一种 Java 枚举类型映射到另一种类型的方法。
  2. 默认情况下,源枚举中的每个常量都映射到目标枚举类型中具有相同名称的常量。如果需要,可以在@ValueMapping注释的帮助下将源枚举中的常量映射到具有另一个名称的常量。源枚举中的几个常量可以映射到目标类型中的同一个常量。
2.4.1.1 枚举映射方法
@Mapper
public interface OrderMapper {
 
    OrderMapper INSTANCE = Mappers.getMapper( OrderMapper.class );
 
    @ValueMappings({
        @ValueMapping(target = "SPECIAL", source = "EXTRA"),
        @ValueMapping(target = "DEFAULT", source = "STANDARD"),
        @ValueMapping(target = "DEFAULT", source = "NORMAL")
    })
    ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}
2.4.1.2 枚举映射方法结果
public class OrderMapperImpl implements OrderMapper {
 
    @Override
    public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) {
        if ( orderType == null ) {
            return null;
        }
 
        ExternalOrderType externalOrderType_;
 
        switch ( orderType ) {
            case EXTRA: externalOrderType_ = ExternalOrderType.SPECIAL;
            break;
            case STANDARD: externalOrderType_ = ExternalOrderType.DEFAULT;
            break;
            case NORMAL: externalOrderType_ = ExternalOrderType.DEFAULT;
            break;
            case RETAIL: externalOrderType_ = ExternalOrderType.RETAIL;
            break;
            case B2B: externalOrderType_ = ExternalOrderType.B2B;
            break;
            default: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );
        }
 
        return externalOrderType_;
    }
}
  1. MapStruct 还具有将任何剩余(未指定)映射映射到默认值的机制。这只能在一组值映射中使用一次,并且仅适用于源。它有两种口味:<ANY_REMAINING>和<ANY_UNMAPPED>。它们不能同时使用。
  2. 在源<ANY_REMAINING>MapStruct 的情况下,将继续将源枚举常量映射到具有相同名称的目标枚举常量。源枚举常量的其余部分将映射到@ValueMappingwith<ANY_REMAINING>源中指定的目标。
  3. MapStruct不会尝试这种基于名称的映射,而是<ANY_UNMAPPED>直接将@ValueMappingwith<ANY_UNMAPPED>源中指定的目标应用于其余部分。
  4. 两个变量,一个是将未指定部分的目标部分不具有和源目标匹配的部分映射,另一部分是将未指定的其余部分全部统一以源常量映射到目标其余部分。
2.4.1.3 枚举映射方法, 和 <ANY_REMAINING>
@Mapper
public interface SpecialOrderMapper {
 
SpecialOrderMapper INSTANCE = Mappers.getMapper( SpecialOrderMapper.class );
 
@ValueMappings({
@ValueMapping( source = MappingConstants.NULL, target = "DEFAULT" ),
@ValueMapping( source = "STANDARD", target = MappingConstants.NULL ),
@ValueMapping( source = MappingConstants.ANY_REMAINING, target = "SPECIAL" )
})
ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}
2.4.1.4 枚举映射方法结果, 和 <ANY_REMAINING>
public class SpecialOrderMapperImpl implements SpecialOrderMapper {
 
    @Override
    public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) {
        if ( orderType == null ) {
            return ExternalOrderType.DEFAULT;
        }
 
        ExternalOrderType externalOrderType_;
 
        switch ( orderType ) {
            case STANDARD: externalOrderType_ = null;
            break;
            case RETAIL: externalOrderType_ = ExternalOrderType.RETAIL;
            break;
            case B2B: externalOrderType_ = ExternalOrderType.B2B;
            break;
            default: externalOrderType_ = ExternalOrderType.SPECIAL;
        }
 
        return externalOrderType_;
    }
}

2.5 高级映射选项

2.5.1 默认值和常量

@Mapper(uses = StringListMapper.class)
public interface SourceTargetMapper {
 
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
 
    @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
    @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
    @Mapping(target = "stringConstant", constant = "Constant Value")
    @Mapping(target = "integerConstant", constant = "14")
    @Mapping(target = "longWrapperConstant", constant = "3001")
    @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
    @Mapping(target = "stringListConstants", constant = "jack-jill-tom")
    Target sourceToTarget(Source s);
}
  1. 如果s.getStringProp() == null,则目标属性stringProperty将设置为 ,"undefined"而不是应用来自 的值
  2. 如果s.getLongProperty() == null,则目标属性longProperty将设置为-1。
  3. 字符串"Constant Value"按原样设置为目标属性stringConstant。该值"3001"被类型转换为Long目标属性的(包装器)类longWrapperConstant。日期属性也需要日期格式。
  4. 该常量"jack-jill-tom"演示了如何StringListMapper调用手写类将破折号分隔的列表映射到List.

2.5.2 表达式

目前仅支持 Java 作为语言。例如,此功能可用于调用构造函数。整个源对象都可以在表达式中使用。应注意仅插入有效的 Java 代码:MapStruct 不会在生成时验证表达式,但在编译期间生成的类中会显示错误。

2.5.2.1 使用表达式的映射方法
@Mapper
public interface SourceTargetMapper {
 
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
 
    @Mapping(target = "timeAndFormat",
         expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
    Target sourceToTarget(Source s);
}
2.5.2.2 通过imports在@Mapper注释上定义来解决类的导入问题

源属性time和format组合成一个目标属性TimeAndFormat。请注意,指定了完全限定的包名称,因为 MapStruct 不负责TimeAndFormat类的导入(除非它在 中明确使用SourceTargetMapper)。

imports org.sample.TimeAndFormat;
 
@Mapper( imports = TimeAndFormat.class )
public interface SourceTargetMapper {
 
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
 
    @Mapping(target = "timeAndFormat",
         expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
    Target sourceToTarget(Source s);
}

2.5.3 默认表达式

默认表达式是默认值和表达式的组合。它们只会在源属性为null时使用

2.5.3.1 使用默认表达式的映射方法
imports java.util.UUID;
 
@Mapper( imports = UUID.class )
public interface SourceTargetMapper {
 
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
 
    @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
    Target sourceToTarget(Source s);
}

只在sourceId为空时为target中的id用java表达式赋值。

其他高阶用法请参考官方文档