MapStruct入门指南

MapStruct是什么

在现在多模块多层级的项目中,应用于应用之间,模块于模块之间数据模型一般都不通用,每层都有自己的数据模型。

这种对象与对象之间的互相转换,目前都是使用get,set方法,或者使用自定义的Beans.copyProperties进行转换。

使用get,set方式会使得编码非常的麻烦,Beans.copyProperties的方式是使用反射的方式,对性能的消耗比较大。

MapStruct就是一个属性映射工具。

与手动编写映射代码相比,MapStruct通过生成繁琐且易于出错的代码来节省时间。约定优于配置的方式,MapStruct使用合理的默认值,但在配置或实现特殊行为时不加理会。

和动态映射框架相比,MapStruct具有以下优点:

  • 通过使用普通方法调用而不是反射来快速执行
  • 编译时类型安全性
  • 构建是清除错误报告
    • 映射不完整(并非所有target属性都被映射)
    • 映射不正确(找不到正确的映射方法或类型转换)

MapStruct配置

基于Mavne

<properties>
  <org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</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>

基于Gradle

plugins {
    id 'net.ltgt.apt' version '0.20'
}
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'

dependencies {
    
    implementation "org.mapstruct:mapstruct:${mapstructVersion}"
    annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"

    // If you are using mapstruct in test code
    testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}

配置选项

MapStruct代码生成器有一些处理器选项配置

  • mapstruct.suppressGeneratorTimesstamp
    • 如果设置为true,将禁止在生成的映射器类中创建时间戳
    • 默认为false
  • mapstruct.suppressGeneratorVersionInfoComment
    • 如果设置为true,将禁止在生成的映射器类中创建属性
    • 默认为false
  • mapstruct.defaultComponentModel
    • 基于生成映射器的组件模型的名称
    • 支持:default,cdi,spring,jsr330
    • 默认为default,使用spring可以使用@Autowired方式注入
  • mapstruct.unmappedTargetPolicy
    • 在未使用source值填充映射方法的target的属性的情况下要应用的默认报告策略。
    • 支持:ERROR(映射代码生成失败),WARN(构建时引起警告),IGNORE(将被忽略)
    • 默认为WARN

配置方式如下
Maven方式

<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>

gradle方式

compileJava {
    options.compilerArgs = [
        '-Amapstruct.suppressGeneratorTimestamp=true',
        '-Amapstruct.suppressGeneratorVersionInfoComment=true'
    ]
}

MapStruct使用

基础应用

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

@Data
public class Source {

    private String testing;

    private String a;

}
public class Target {

    private String test;

    private String a;

    public String getTest() {
        return test;
    }

    public void setTest(String test) {
        this.test = test;
    }

    public String getA() {
        return a;
    }

    public void setA(String a) {
        this.a = a;
    }
}

@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface SourceTargetMapper {

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

    @Mapping(target = "test", source = "testing")
    Target toTarget(Source s);
}

@Mapper注解将使用MapStruct代码生成器创建对应的映射类

在生成的方法实现中,source类型的所有可读属性都将复制到target类型的相应属性中

  • 当一个属性于其target实体对应的名称相同时,它将被隐式映射,
  • 当属性在target实体中具有不同的名称时,可以通过@Mapping注释指定其名称。

生成的代码如下:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2019-12-19T15:10:51+0800",
    comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_172 (Oracle Corporation)"
)
public class SourceTargetMapperImpl implements SourceTargetMapper {

    @Override
    public Target toTarget(Source s) {
        if ( s == null ) {
            return null;
        }

        Target target = new Target();

        target.setTest( s.getTesting() );
        target.setA( s.getA() );

        return target;
    }
}

MapStruct的原理是生成和我们自己写的代码一样的代码,这意味着这些值是通过简单的getter/setter调用而不是反射或类似的方法从source类复制到target类的。使得MapStruct的性能会比动态框架更加优秀

添加自定义转换方法

在某种情况下可能需要手动实现从一种类型到另一种类型的特定映射。解决这个问题的方法是实现自定义方法,然后该类由MapStruct生成的映射器使用

自定义方法可以写在接口中,使用Java8的接口方法,也可以写在其他类中,在接口的@Mapper注解中引入

方式一: Car中有类型为Person的属性,CarDto中有类型为PersonDto的属性

@Mapper
public interface CarMapper {

    @Mapping(...)
    ...
    CarDto carToCarDto(Car car);

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

方式二、通过外部引入自定义的转换方式,将同名的Optional类型属性和非Optional类型属性相互转换

public class OptionalMapper {
    public <T> T toOptional(Optional<T> test) {
        if (test == null) {
            return null;
        }
        return test.orElse(null);
    }

    public <T> Optional<T> formOptional(T test) {
        if (test == null) {
            return Optional.empty();
        }
        return Optional.of(test);
    }
}
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR, uses = OptionalMapper.class)
public interface SourceTargetMapper {

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

    Target toTarget(Source s);
}

具有多个source参数的映射

有时候需要将多个source对象聚合成一个target的场景
这个和普通的一对一很相似,在@Mapping注解中定义对应source和target对应的属性即可,参数可以是基础数据类型和String,映射时使用对象名称进行。

@Mapper
public interface AddressMapper {

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

更新类的属性

在一些场景里面,映射不需要创建新的target,而是更新该类型的现有实例,可以使用注解@MappingTarget注释参数,来表明该参数是映射的target。下面是例子:

@Data
public class MammalEntity {

    private Long numberOfLegs;
    private Long numberOfStomachs;
}
@Data
public class MammalDto {

    private Integer numberOfLegs;
}
@Mapper
public interface SourceTargetMapper {

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

    @Mapping( target = "numberOfStomachs", source = "numberOfStomachs" )
    void toTarget(MammalDto source, Long numberOfStomachs, @MappingTarget MammalEntity target);
}

生成的映射类如下:

public class SourceTargetMapperImpl implements SourceTargetMapper {

    @Override
    public void toTarget(MammalDto source, Long numberOfStomachs, MammalEntity target) {
        if ( source == null && numberOfStomachs == null ) {
            return;
        }

        if ( source != null ) {
            if ( source.getNumberOfLegs() != null ) {
                target.setNumberOfLegs( source.getNumberOfLegs().longValue() );
            }
            else {
                target.setNumberOfLegs( null );
            }
        }
        if ( numberOfStomachs != null ) {
            target.setNumberOfStomachs( numberOfStomachs );
        }
    }
}

映射对象引用

通常一个对象不仅具有基本属性,而且还引用了其他对象
例如:Car类包含一个Person对象,该对应应该映射到CarDto中的PersionDto.
这种情况下,只需为引用的对象类型定义一个映射方法。

@Mapper
public interface CarMapper {

    CarDto carToCarDto(Car car);

    PersonDto personToPersonDto(Person person);
}

生成映射方法是,MapStruct将为source对象和target中的每个属性对应以下规则:

  1. 如果source和target属性具有相同的类型,则简单的将source复制到target,如果属性是一个集合,则该集合的副本将设置到target属性中,集合类型生成代码如下:

    List<Integer> list = s.getListTest();
        if ( list != null ) {
            target.setListTest( new ArrayList<Integer>( list ) );
        }
  2. 如果source属性和target属性类型不同,检查是否存在另一种映射方式,该映射方法将source属性的类型作为参数类型并将target属性的类型作为返回类型。如果存在这样的方法,它将在生成的映射实现中调用。
  3. 如果不存在这样的方法,MapStruct将查看是否存在属性的source类型和target类型的内置转换。在这种情况下,生成的映射代码将应用此转换。
  4. 如果找不到这种方法,MapStruct将尝试生成自动子映射方法,该方法将在source属性和target属性之间进行映射。
  5. 如果MapStruct无法创建基于名称的映射方法,则会在构建时引发错误,指示不可映射的属性及其路径。

隐式类型转换

MapStruct会自动处理类型转换,例如:如果某个属性int在souce中类型为String,则生成的代码将分别通过调用String.valueOf(int),和Integer.parseInt(String)执行转换。

以下转换将自动应用:

  • 在所有Java原语数据类型及其对应的包装器类型之间(例如在int和之间Integer,boolean以及Boolean等)之间。生成的代码是已知的null,即,当将包装器类型转换为相应的原语类型时,null将执行检查.
  • 在所有Java数字类型和包装器类型之间,例如在int和long或byte和之间Integer。

    从较大的数据类型转换为较小的数据类型(例如从long到int)可能会导致值或精度损失。在Mapper和MapperConfig注释有一个方法typeConversionPolicy来控制警告/错误。由于向后兼容的原因,默认值为“ ReportingPolicy.IGNORE”。

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

    @IterableMapping(numberFormat = "$#.00")
    List<String> prices(List<Integer> prices);

    自动生成的映射类方法

    @Override
    public List<String> prices(List<Integer> prices) {
        if ( prices == null ) {
            return null;
        }
    
        List<String> list = new ArrayList<String>( prices.size() );
        for ( Integer integer : prices ) {
            list.add( new DecimalFormat( "$#.00" ).format( integer ) );
        }
    
        return list;
    }
  • 在enum类型和之间String。
  • 在大数字类型(java.math.BigInteger,java.math.BigDecimal)和Java基本类型(包括其包装器)以及字符串之间。java.text.DecimalFormat可以指定理解的格式字符串
  • 在java.util.Date/ XMLGregorianCalendar和之间String。java.text.SimpleDateFormat可以通过以下dateFormat选项指定可理解的格式字符串:
    java @IterableMapping(dateFormat = "dd.MM.yyyy") List<String> stringListToDateList(List<Date> dates);

    自动生成的映射类方法

    @Override
    public List<String> stringListToDateList(List<Date> dates) {
        if ( dates == null ) {
            return null;
        }
    
        List<String> list = new ArrayList<String>( dates.size() );
        for ( Date date : dates ) {
            list.add( new SimpleDateFormat( "dd.MM.yyyy" ).format( date ) );
        }
    
        return list;
    }

    高级映射选项

默认值和常量

如果相应的source属性为null,则可以指定将target的属性设置为默认值。在任何情况下都可以指定常量来设置这样的预定义值。
默认值和常量使用String来指定,当target类型是基础类型或者包装类时,映射类会进行自动的类型转换,以匹配target属性所需的类型

    @Mapping(target = "name", constant = "常量")
    @Mapping(target = "quantity", defaultValue = "1L")
    OrderItem toOrder(OrderItemDto orderItemDto);
    @Override
    public OrderItem toOrder(OrderItemDto orderItemDto) {
        if ( orderItemDto == null ) {
            return null;
        }

        OrderItem orderItem = new OrderItem();

        if ( orderItemDto.quantity != null ) {
            orderItem.setQuantity( orderItemDto.quantity );
        }
        else {
            orderItem.setQuantity( (long) 1L );
        }

        orderItem.setName( "常量" );

        return orderItem;
    }
表达式

该功能对于调用函数很有用,整个source对象在表达式中都是可被调用的。

MapStruct不会生成验证表达式,但在编译过程中错误会显示在生成的类中

imports com.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);
}

MapStruct不会处理类的导入,但是可以在@Mapper注解中使用imports中引入

默认表达式

默认表达式是默认值和表达式的组合,当source属性为null是才使用

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

配置的继承

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

@Mapper
public interface CarMapper {

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

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

上面的示例中声明了carDtoToCar()具有配置的映射方法,在进行updateCar的时候使用注解@InheritConfigurationMapStruct就可以搜索可以继承的方法进行映射。
如果存在多个可以用来继承的方法的时候,就需要在当前的映射器中定义需要继承的方法:@InheritConfiguration( name = "carDtoToCar" )

逆映射

在双向映射的情况下,正向方法和返现方法的映射规则通常都是相似的,就可以简单的切换source和targe加上继承的注解,进行逆映射。

    @Mapping(source = "orders", target = "orderItems")
    @Mapping(source = "customerName", target = "name")
    Customer toCustomer(CustomerDto customerDto);

    @InheritInverseConfiguration
    CustomerDto fromCustomer(Customer customer);

总结

总结一下MapStruct的注解关键词

@Mapper 在接口上添加这个注解,MapStruct才会去实现该接口
    using: 应用外部映射类
    import: 导入表达式使用的类
    componentModel: 指定实现类的类型
        default:默认,通过Mappers.getMapper(Class)方式获取实例对象
        spring: 自动添加注解@Component,通过@Autowired方式注入
@Mapping: 属性映射,若source对象与target对象名称一致,会自动映射对应属性
    source: 源属性
    target: 目标属性
    deteFormat: String 到Date日期之间相互转换,通过SimpleDateFormat
    ignore: 忽略这个字段
@Mappings:配置多个@Mapping    
@MappingTarget: 用于更新已有对象
@InheritConfiguration: 用于继承配置

文章中介绍了MapStruct中个人觉得实用的功能,此外还有很多高级特性在文章中没有进行描述,感兴趣可以去阅读MapStruct官方文档了解更多。
相对于我们目前对象转换的方式来说,MapStruct有着和Lombok一样提高工作效率的能力。可以使我们从繁琐的transfer中脱离出来,专心于业务代码的实现。
MapStruct同样也兼容Lombok
Maven中配置如下

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version> <!-- or newer version -->
                <configuration>
                    <source>1.8</source> <!-- depending on your project -->
                    <target>1.8</target> <!-- depending on your project -->
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <!-- other annotation processors -->
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${org.projectlombok.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>

gradle中配置如下

dependencies {
    compile "org.mapstruct:mapstruct:${mapstructVersion}", "org.projectlombok:lombok:${lombokVersion}"
    testCompile 'junit:junit:4.12'
    apt "org.mapstruct:mapstruct-processor:${mapstructVersion}", "org.projectlombok:lombok:${lombokVersion}"
}

对于性能来说MapStruct的性能在映射框架中同样也属于佼佼者的地位。
MapStruct入门指南

上一篇:Mapstruct 使用教程


下一篇:MapStruct生成继承类对象的Spring容器对象属性注入问题源码分析