高性能的属性拷贝-MapStruct

986 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

简介

        MapStruct可以说是一个代码开发的辅助工具,能简便的帮我们实现bean属性的复制。MapStruct主要是在代码编译阶段依据开发人员的配置,将对应的pojo类做相应的对象转换,帮助开发人员直接转换生成了新的pojo类对象。

原理

        相比以往做属性拷贝,我们通常都会直接使用BeanUtils工具类,BeanUtils虽然也能实现相同的效果开发起来也更简单,但是我们忽略了它的性能,BeanUtils的实现原理是通过反射获取对象属性去实现映射传递参数到新对象的,这种情况性能就会大大降低,而且对于相同属性不同类型的转换就会出现异常,对于不同属性名含义相同的也没办法赋值。

        简单了解完BeanUtils并不是那么友好,那么MapStruct的优势就来了,MapStruct的实现原理是在编译阶段直接帮我们实现了getter、setter方法去对两个对象属性赋值,还可以通过指定配置属性的映射关系支持不同名的属性映射,最终根据MapStruct配置生成编译后的java文件,所以MapStruct能够在不影响性能的情况下极大的减少了开发人员的工作量。

使用方式

pom工程引入maven依赖

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.3.1.Final</version>
</dependency>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.3.1.Final</version>
</dependency>

定义MapStruct接口,并加上注解 @Mapper(componentModel = "spring")

场景应用

基础pojo类定义,通常比较规范的开发当中都会把pojo按层次细分,下面我们就通过PO、DTO、VO的转换来看看MapStruct到底是如何使用的。

/**
* 汽车PO类
*/
public class Car {
    // 品牌
    private String brand;
    // 类型(1:轿车、2:SUV)
    private Integer type;
    // 颜色
    private String color;
    // 排量
    private String displacement;
    // 生产日期
    private Date productDate;
}
/**
* 汽车DTO类
*/
public class CarDto {
    // 品牌
    private String brand;
    // 类型
    private Integer type;
    // 颜色
    private String color;
    // 排量
    private String displacement;
    // 生产日期
    private Date productDate;
}
/**
* 汽车VO类
*/
public class CarVo {
    // 品牌
    private String brand;
    // 类型
    private String type;
    // 颜色
    private String color;
    // 排量
    private String displacement;
    // 生产日期
    private String productDate;
}

同名属性映射

如上Car类与CarDto类的属性都是同名同类型的,只需在定义MapStruct接口添加一个转换方法将Car对象转换为CarDto对象,如下:

@Mapper(componentModel = "spring")
public interface CarMapStruct {
    // Car类型转换为CarDto
    CarDto carToCarDto(Car car);
}

上面的代码编译后就生成一个CarMapStruct接口的实现类,CarMapStructImpl类同时实现转换方法carToCarDto如下:

image.png

由此可见编译后的代码已帮我们实现两个pojo类之间的数据getter、setter赋值。

测试结果:

image.png

非同名属性映射

我们将CarDto类brand属性改为brandName,然后增加配置@Mapping(source = "brand", target = "brandName")将Car类brand属性赋值到brandName

public class CarDto {
    // 品牌
    private String brandName;
    // ......
}
@Mapper(componentModel = "spring")
public interface CarMapStruct {
    // 配置将brand赋值到brandName属性
    @Mapping(source = "brand", target = "brandName")
    CarDto carToCarDto(Car car);
}

重新编译结果如下:

image.png

测试结果:

image.png

集合映射

集合映射跟实体类映射都是一样的,只不过在实体类上增加集合类型即可实现:

@Mapper(componentModel = "spring")
public interface CarMapStruct {
    @Mapping(source = "brand", target = "brandName")
    CarDto carToCarDto(Car car);

    List<CarDto> carToCarDtoList(List<Car> car);
}

注:需要注意的是,集合的映射会先生成集合泛型类的实体映射,如果我们没有定义该泛型类的映射,那么会默认按sourceToTarget的形式作为方法名,source、target即原始类名以及目标类型最终组合成驼峰形式的方法名,如上我们以及提前定义好方法carToCarDto,就不会在自动生成,而是默认使用我们定义的方法:

image.png

测试结果:

image.png

嵌套对象映射

嵌套对象映射就是在我们需要映射的pojo类中的某些属性也是pojo对象,这种情况我们如果按照原有的方式做映射不做额外配置的话,那么MapStruct就会默认帮我们实现自动映射,之间在Car、CarDto类中增加Wheel、WheelDto类属性:

@Data
public class Wheel {
    // 尺寸大小
    private int size;
    // 品牌
    private String brand;
    // 说明
    private String description;
}

@Data {
    // ...
    private Wheel wheel;
}
@Data
public class WheelDto {
    // 尺寸大小
    private int size;
    // 品牌
    private String brand;
    // 说明
    private String description;
}

@Data
public class CarDto {
    // ...
    private WheelDto wheel;
}

MapStruct接口无需更改,编译后结果如下,自动帮我们生成转换方法wheelToWheelDto: image.png

测试结果:

image.png

另一种方案就是引入其他的MapStruct接口,我们可以优先定义好WheelMapStruct接口,然后在原有的CarMapStruct接口上的@Mapper注解增加引用uses = {WheelMapStruct.class},然后在WheelMapStruct接口上定义好Wheel于WheelDto的转换接口就行:

@Mapper(componentModel = "spring")
public interface WheelMapStruct {

    WheelDto wheelToWheelDto(Wheel wheel);
}

image.png

编译后的CarMapStruct接口生成的CarMapStructImpl类中会自动注入WheelMapStruct接口: image.png

自定义映射转换

MapStruct可以根据个人需要对某个属性做转换,例如有些数据字典要转换成可视化的数据时,可能使用会更加方便,我们只需在@Mapping注解上指定转换方法qualifiedByName="typeTransform"即可

@Mapper(componentModel = "spring")
public interface CarMapStruct {

    @Mapping(source = "brand", target = "brandName")
    CarDto carToCarDto(Car car);

    List<CarDto> carToCarDtoList(List<Car> car);

    @Mapping(source = "type", target = "type", qualifiedByName = "typeTransform")
    CarVo carDtoToCarVo(CarDto carDto);
    
    @Named("typeTransform")
    default String getType(Integer type) {
        if (1 == type) {
            return "轿车";
        }
        if (2 == type) {
            return "SUV";
        }
        return "汽车";
    }
}

编译后的结果就是在setType的时候通过我们自定义的方法先将值进行一轮转换。 image.png

测试结果:

image.png

其他映射

另外在pojo映射关系@Mapping注解上还有一些其他参数配置:

  • dateFormat:涉及到Date类型与String类型相互转换的,可以指定日期转换格式,否则会默认使用SimpleDateFormat的默认日期格式:
@Mapping(source = "productDate", target = "productDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
CarVo carDtoToCarVo(CarDto carDto);

image.png

  • constant:某些属性我们希望他的结果都是一个固定值,那么就可以使用常量值设置。(注:设置了constant参数时,不能有source否则编译会出错
@Mapping(target = "color", constant = "黑色")
CarDto carToCarDto(Car car);

image.png

  • defaultValue:这个参数使用范围应该会相对多一些,很多时候我们不知道一个属性转换后是否有值,但又不希望它是一个空值,这里我们就可以使用defaultValue做默认值设置。(注:defaultValue仅支持String类型,且是值为null,如果值为空字符串并不被认为是空
@Mapping(source = "color", target = "color", defaultValue = "black")
CarDto carToCarDto(Car car);

image.png

  • ignore:可以设置ignore = true来忽略某个属性的映射,默认为false。
@Mapping(target = "color", ignore = true)
CarDto carToCarDto(Car car);

编译后会忽略掉color的setter方法。

总结

MapStruct也是我今年才开始用的,以往在日常开发过程中很多性能上的问题都没太注意,慢慢的自己也跟着团队越来越重视性能这块的细节了。MapStruct在刚开始用的时候配置起来可能会觉得比BeanUtils繁琐许多,各种pojo对象互相转换都需要进行配置,但同时为我们开发的程序提供了良好的性能,同时也减轻了我们逐个编写getter、setter的烦恼,还是希望各位能够亲自动手去敲一敲,熟话说熟能生巧就是这个道理,用多永久习惯了自然就方便许多了。