源码解析-MapStruct使用实战

2,657 阅读6分钟

1.简介

一个能进行领域对象之间转换的好工具,可通过注解的方式完成对象属性拷贝,可用于实际业务场景中功能的PO\VO\DTO\DO层之间数据转换。

2.Maven配置

       直接添加maven引用即可使用,因为是基于编译期去做代码生成的,需要在编译插件中添加注解处理器。

<properties>
    <org.mapstruct.version>1.1.0.Final</org.mapstruct.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-jdk8</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <!-- 编译插件 -->
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

 还可以根据项目需要和个人口味进行选项配置。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>${org.mapstruct.version}</version>
            </path>
        </annotationProcessorPaths>
        <compilerArgs>
            <compilerArg>
                -Amapstruct.suppressGeneratorTimestamp=true
            </compilerArg>
            <compilerArg>
                -Amapstruct.suppressGeneratorVersionInfoComment=true
            </compilerArg>
        </compilerArgs>
    </configuration>
</plugin>

3. mapper的定义

3.1 定义mapper

@Mapper
public interface CarMapper {

    @Mappings({
        @Mapping(source = "make", target = "manufacturer"),
        @Mapping(source = "numberOfSeats", target = "seatCount")
    })
    CarDto carToCarDto(Car car);

    @Mapping(source = "name", target = "fullName")
    PersonDto personToPersonDto(Person person);
}

3.2 添加mapper的自定义方法

@Mapper
public interface CarMapper {

    @Mappings({...})
    CarDto carToCarDto(Car car);

    default PersonDto personToPersonDto(Person person) {
        //hand-written mapping logic
    }
}

3.3 配置有多个参数源的mapper方法

  • 使用@Mapping注释时,需要指定属性所在的参数

  • 所有的源参数都是null时,那么映射方法返回null

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

3.4 更新现有的bean实例

  • 使用@MappingTarget更新目标对象

    @Mapper public interface CarMapper { void updateCarFromDto(CarDto carDto, @MappingTarget Car car); }

3.5 实例字段访问的映射

  • 支持public没有getter/setter的字段的映射

  • mapstruct无法找到合适的属性的getter/setter方法,MapStruct将使用这些字段作为读写访问器

    public class Customer { private Long id; private String name; }

    public class CustomerDto { public Long id; public String customerName; }

    @Mapper public interface CustomerMapper {

    CustomerMapper MAPPER = Mappers.getMapper( CustomerMapper.class );
    
    @Mapping(source = "customerName", target = "name")
    Customer toCustomer(CustomerDto customerDto);
    
    @InheritInverseConfiguration
    CustomerDto fromCustomer(Customer customer);
    

    }

    // GENERATED CODE 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();
        // ...
    }
    

    }

4.生成一个映射器

4.1 Mapper工厂

  • Mapper实例可以通过org.mapstruct.factory.Mappers类来获取。只需调用该getMapper()方法,传递映射器的接口类型即可返回:

    CarMapper mapper = Mappers.getMapper( CarMapper.class );

    @Mapper public interface CarMapper {

    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    
    CarDto carToCarDto(Car car);
    

    }

    Car car = ...; CarDto dto = CarMapper.INSTANCE.carToCarDto(car);

4.2 依赖注入

  • 如CDI (上下文和依赖注入的Java TM EE) 可以指定生成映射器类的组件模型,该组件模型应该基于通过@Mapper#componentModel或使用处理器选项

  • 目前支持CDI和Spring (后者通过自定义注释或使用JSR 330注释) ,以下显示了使用CDI的示例:

    @Mapper(componentModel = "cdi") public interface CarMapper { CarDto carToCarDto(Car car); }

生成的映射器实现将使用@ApplicationScoped注释标记,因此可以使用@Inject注释将其注入到字段,构造函数参数等中:

@Inject
private CarMapper mapper;

5.数据类型转换

5.1 隐式类型转换

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

目前,以下转换会自动应用:

  • 所有Java基本数据类型及其相应的包装类型,例如之间int和Integer,boolean和Boolean、int和long、byte和Integer。

  • 所有Java基本类型之间(包括其包装)和String之间,例如int和String或Boolean和String,java.text.DecimalFormat可以指定格式化字符串。

    @Mapper public interface CarMapper {

    @Mapping(source = "price", numberFormat = "$#.00")
    CarDto carToCarDto(Car car);
    
    @IterableMapping(numberFormat = "$#.00")
    List<String> prices(List<Integer> prices);
    
    @Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")
    CarDto carToCarDto(Car car);
    
    @IterableMapping(dateFormat = "dd.MM.yyyy")
    List<String> stringListToDateList(List<Date> dates);
    

    }

5.2 制嵌套的bean映射

@Mapper
public interface FishTankMapper {

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

5.3 调用其他映射器

MapStruct将查找将Date对象映射到String的方法,在DateMapper类上找到它并生成一个  asString()用于映射该manufacturingDate属性的调用

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(uses=DateMapper.class)
public class CarMapper {

    CarDto carToCarDto(Car car);
}

6 集合映射

  • 集合类型(List,Set等等)以相同的方式映射bean类型,通过定义与在映射器接口所需的源和目标类型的映射方法。MapStruct支持Java Collection Framework中的各种可迭代类型。

  • 生成的代码将包含一个遍历源集合的循环,转换每个元素并将其放入目标集合中。如果在给定的映射器或其使用的映射器中找到了集合元素类型的映射方法,则会调用此方法来执行元素转换。或者,如果存在源和目标元素类型的隐式转换,则将调用此转换例程。以下是一个例子:

6.1 Map容器映射

public interface SourceTargetMapper {

    @MapMapping(valueDateFormat = "dd.MM.yyyy")
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}

//GENERATED CODE
@Override
public Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> source) {
    if ( source == null ) {
        return null;
    }
    Map<Long, Date> map = new HashMap<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;
}

6.2 用于集合映射的实现类型

  • Iterable -> ArrayList

  • Collection -> ArrayList 

  • List -> ArrayList 

  • Set -> ArrayList 

  • SortedSet -> TreeSet

  • NavigableSet -> TreeSet 

  • Map -> HashMap 

  • SortedMap -> TreeMap 

  • NavigableMap -> TreeMap

7.映射流

使用在映射器接口中已定义的源和目标类型定义映射方法。

@Mapper
public interface CarMapper {

    Set<String> integerStreamToStringSet(Stream<Integer> integers);

    List<CarDto> carsToCarDtos(Stream<Car> cars);

    CarDto carToCarDto(Car car);
}

@Override
public Set<String> integerStreamToStringSet(Stream<Integer> integers) {
    if ( integers == null ) {
        return null;
    }

    return integers.stream().map( integer -> String.valueOf( integer ) )
        .collect( Collectors.toCollection( HashSet<String>::new ) );
}

@Override
public List<CarDto> carsToCarDtos(Stream<Car> cars) {
    if ( cars == null ) {
        return null;
    }

    return integers.stream().map( car -> carToCarDto( car ) )
        .collect( Collectors.toCollection( ArrayList<CarDto>::new ) );
}

8.枚举类型的映射

支持将一个Java枚举类型映射到另一个类型的方法。默认情况下,源枚举中的每个常量映射到目标枚举类型中具有相同名称的常量。

@Mapper
public interface OrderMapper {

    OrderMapper INSTANCE = Mappers.getMapper( OrderMapper.class );

    @ValueMappings({
        @ValueMapping(source = "EXTRA", target = "SPECIAL"),
        @ValueMapping(source = "STANDARD", target = "DEFAULT"),
        @ValueMapping(source = "NORMAL", target = "DEFAULT")
    })
    ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}

// GENERATED CODE
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_;
    }
}

9 . 对象工厂

  • 生成的代码将调用默认的构造函数来实例化目标类型

  • 自定义对象工厂获得目标类型的实例。

    public class DtoFactory {

     public CarDto createCarDto() {
         return // ... custom factory logic
     }
    

    }

    @Mapper(uses = { DtoFactory.class, EntityFactory.class, CarMapper.class } ) public interface OwnerMapper {

    OwnerMapper INSTANCE = Mappers.getMapper( OwnerMapper.class );
    
    void updateOwnerDto(Owner owner, @MappingTarget OwnerDto ownerDto);
    
    void updateOwner(OwnerDto ownerDto, @MappingTarget Owner owner);
    

    }

10. 高级用法

10.1 默认值和常量

@Mapper(uses = StringListMapper.class)
public interface SourceTargetMapper {

    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

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

10.2 表达式

MapStruct不会在生成时验证表达式,但在编译过程中会在生成的类中显示错误。可以通过imports在@Mapper注释上定义来解决。

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

10.3 确定结果类型

当结果类型具有继承关系时,选择映射方法(@Mapping)或工厂方法(@BeanMapping)可能变得不明确。假设一个苹果和一个香蕉,都是水果的专业化。

@BeanMapping#resultType派上用场了。它控制工厂方法选择或在没有工厂方法的情况下选择要创建的返回类型。

@Mapper( uses = FruitFactory.class )
public interface FruitMapper {

    @BeanMapping( resultType = Apple.class )
    Fruit map( FruitDto source );

}

public class FruitFactory {

    public Apple createApple() {
        return new Apple( "Apple" );
    }

    public Banana createBanana() {
        return new Banana( "Banana" );
    }
}

10.4 控制“空”参数的映射结果

当映射方法的源参数等于时,MapStruct提供对要创建的对象的控制null。默认情况下null会返回。但是,通过指定nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT上

@BeanMapping,@IterableMapping,@MapMapping,或全局上@Mapper或@MappingConfig,映射结果可以被改变以返回空默认值。这意味着:

Bean映射:将返回一个“空的”目标bean,除常量和表达式外,当它们出现时将被填充。

基元:基元的默认值将被返回,例如false for boolean或0 for int。

Iterables / Arrays:一个空的迭代将被返回。

map:将返回一个空的map。

该策略以分层方式工作。nullValueMappingStrategy映射方法级别上的设置将覆盖@Mapper#nullValueMappingStrategy,@Mapper#nullValueMappingStrategy并将覆盖@MappingConfig#nullValueMappingStrategy

@BeanMapping(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT)
StudentVo toStudentVo(UserAllDto userDto, UserInfoDo userInfoDo);

11. 映射配置高级使用

11.1 映射配置继承

方法级配置注解,例如@Mapping,@BeanMapping,@IterableMapping,等等,都可以继承从一个映射方法的类似使用注释方法@InheritConfiguration:

一种方法如果所有类型的A(源类型和结果类型)都可以分配给相应类型的B,则A可以继承另一个方法B的配置。

考虑进行继承的方法需要在当前映射器,超类/接口或共享配置接口中定义(如共享配置中所述)。

如果有多个方法可用作继承的源,则必须在注释中指定方法名称:@InheritConfiguration( name = "carDtoToCar" )。

一种方法,可以使用@InheritConfiguration和覆盖或通过另外施加修改的配置@Mapping,@BeanMapping等等。

@Mapper
public interface CarMapper {

    @Mapping(target = "numberOfSeats", source = "seatCount")
    Car carDtoToCar(CarDto car);

    @InheritConfiguration
    void carDtoIntoCar(CarDto carDto, @MappingTarget Car car);
}

11.2 逆向映射

       在双向映射,例如从实体到DTO和从DTO到实体的情况下,用于前向方法和反向方法的映射规则通常相似,并且可以简单地通过切换来反转source和target。

@Mapper
public interface CarMapper {

    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToDto(Car car);

    @InheritInverseConfiguration
    Car carDtoToCar(CarDto carDto);
}

11.3 共享配置

  • MapStruct提供了通过指向中心接口来定义共享配置的可能性@MapperConfig。为了使映射器使用共享配置,需要在@Mapper#config属性中定义配置接口。

  • @MapperConfig注释具有相同的属性@Mapper注释。任何未通过的属性@Mapper都将从共享配置继承。指定@Mapper的属性优先于通过引用的配置类指定的属性。列表属性如uses简单的组合:

    @MapperConfig( uses = CustomMapperViaMapperConfig.class, unmappedTargetPolicy = ReportingPolicy.ERROR, mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG ) public interface CentralConfig {

    // Not intended to be generated, but to carry inheritable mapping annotations:
    @Mapping(target = "primaryKey", source = "technicalKey")
    BaseEntity anyDtoToEntity(BaseDto dto);
    

    }

    @Mapper(config = CentralConfig.class, uses = { CustomMapperViaMapper.class } ) // Effective configuration: // @Mapper( // uses = { CustomMapperViaMapper.class, CustomMapperViaMapperConfig.class }, // unmappedTargetPolicy = ReportingPolicy.ERROR // ) public interface SourceTargetMapper { ... }