MapStruct

发布于 2022-10-07  4.05k 次阅读


1. MapStruct的简介

MapStruch官方定义:

  • 是什么?:MapStruct是一个代码生成器,它基于约定优于配置的方法,极大地简化了Java bean类型之间映射的实现。生成的映射代码使用简单的方法调用,因此快速、类型安全且易于理解
  • 为什么?:多层应用开发通常需要在不同的对象模型(例如实体和dto)之间进行映射。编写这样的映射代码是一项繁琐且容易出错的任务。MapStruct的目标是通过尽可能地自动化来简化这项工作。与其他映射框架相比,MapStruct在编译时生成bean映射,这确保了高性能,允许快速的开发人员反馈和彻底的错误检查
  • 怎么做?:MapStruct是一个插入Java编译器的注释处理器,可以在命令行构建(Maven、Gradle等)中使用,也可以在您首选的IDE中使用。MapStruct使用了合理的默认值,但在配置或实现特殊行为时却不太方便

MapStrush的使用场景:VO、BO、DTO、DO等不同数据或业务实体之间转换

不同实体或模型的Convert解决方案:

名称 描述
MapStruct 基于jsr269实现在编译期间生成代码、性能高、精细控制、解耦
orika 能够精细控制、解耦
Spring的BeanUtils体系 简单易用,但不能对属性进行定制处理

2. 传统方式解决POJO的转换

以一个汽车DTO和VO为例,将DTO转换为VO

① CarDTO

@Data
public class CarDTO {

    //编号
    private Long id;

    //车辆编号
    private String vin;

    //裸车价格
    private double price;

    //上路价格
    private double totalPrice;

    //生成日期
    private Date publishDate;

    //品牌名称
    private  String brand;

    //车辆零件列表
    private List<PartDTO> partDTOS;

    //汽车司机
    private DriverDTO driverDTO;
}

② DriverDTO

@Data
@AllArgsConstructor
@NoArgsConstructor
public class DriverDTO { //司机

    private Long id;

    private String name;

}

③ PartDTO

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PartDTO { //零件

    private Long partId;

    private String partName;

}

④ CarDriverVO

由于前端显示不需要携带零件对象和司机对象,只需要知道是否有零件和只需要知道司机姓名,所以创建一个VO对象进行前端响应

@Data
public class CarDriverVO {
    //编号
    private Long id;

    //车辆编号
    private String vin;

    //裸车价格
    private double price;

    //上路价格
    private double totalPrice;

    //生成日期
    private Date publishDate;

    //品牌名称
    private  String brand;

    //是否配置了零件
    private boolean partDTOS;

    //汽车司机名称
    private String driverName;
}

将DTO转化为VO

public class test {

    public static void main(String[] args) {

        CarDTO carDto = getCarDto();

        CarDriverVO carDriverVO = new CarDriverVO();
        carDriverVO.setId(carDto.getId());
        carDriverVO.setVin(carDto.getVin());
        carDriverVO.setPrice(carDto.getPrice());
        carDriverVO.setTotalPrice(carDto.getTotalPrice());
        carDriverVO.setPublishDate(carDto.getPublishDate());

       boolean par = carDto.getPartDTOS()!=null && !carDto.getPartDTOS().isEmpty();
       carDriverVO.setPartDTOS(par);
       carDriverVO.setDriverName(carDto.getDriverDTO().getName());

    }

    //实在DTO并返回
    public static CarDTO getCarDto(){

        CarDTO carDTO = new CarDTO();
        carDTO.setId(1L);
        carDTO.setVin("湘Ba8921");
        carDTO.setPrice(182382.52);
        carDTO.setTotalPrice(200000.00);
        carDTO.setPublishDate(Date.valueOf(LocalDate.now()));
        carDTO.setBrand("日产");

        List<PartDTO> array= new ArrayList<>();
        PartDTO partDTO = new PartDTO();
        partDTO.setPartId(1L);
        partDTO.setPartName("方向盘");
        array.add(partDTO);

        DriverDTO driverDTO = new DriverDTO();
        driverDTO.setId(1L);
        driverDTO.setName("李某某");

        carDTO.setPartDTOS(array);
        carDTO.setDriverDTO(driverDTO);

        return carDTO;
    }
}

缺点:

  • 可复用性低,代码不简洁
  • 与业务代码耦合性高

3. MapStruct的初体验

步骤:

  1. 引入依赖
  2. 新建一个抽象类或者接口并标注@Mapper
  3. 写一个转换方法
  4. 获取对象并使用

① 引入依赖(版本可选)

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

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

② 新建一个抽象类并写一个转换方法

@Mapper
public abstract class CarConvert {

    //获取当前对象
    public static  CarConvert INSTANCE = Mappers.getMapper(CarConvert.class);

    //转换方法,参数为原实体对象,返回值为结果对象,方法为转换操作
    public abstract CarDriverVO CarDTOToVO(CarDTO carDTO);
}

③ 测试使用

public class Test1 {

    public static void main(String[] args) {

        CarDriverVO carConvert =CarConvert.INSTANCE.CarDTOToVO(getCarDto());
        System.out.println(carConvert);


    }
    //实在DTO并返回
    public static CarDTO getCarDto(){

        CarDTO carDTO = new CarDTO();
        carDTO.setId(1L);
        carDTO.setVin("湘Ba8921");
        carDTO.setPrice(182382.52);
        carDTO.setTotalPrice(200000.00);
        carDTO.setPublishDate(Date.valueOf(LocalDate.now()));
        carDTO.setBrand("日产");

        List<PartDTO> array= new ArrayList<>();
        PartDTO partDTO = new PartDTO();
        partDTO.setPartId(1L);
        partDTO.setPartName("方向盘");
        array.add(partDTO);

        DriverDTO driverDTO = new DriverDTO();
        driverDTO.setId(1L);
        driverDTO.setName("李某某");

        carDTO.setPartDTOS(array);
        carDTO.setDriverDTO(driverDTO);

        return carDTO;
    }
}

结果:

CarDriverVO(id=1, vin=湘Ba8921, price=182382.52, totalPrice=200000.0, publishDate=2022-10-06, brand=日产, partexites=false, driverName=null)

4. 自定义指定映射

MapStruct的默认映射规则:

    1.同类型且同名的属性,会自动映射

    2.mapstruct会自动进行类型转化

  • 8种基本类型和它们对应包装类型之间的转换
  • 8种基本类型和String类型之间的转换
  • 日期类型和String类型之间的转换

@Mappings和@Mapping的功能:自定义指定映射规则

  • @Mappings:@Mapping的集合注解
  • @Mapping:指定具体的映射规则

@Mappings的属性:

  • Mapping[] value():@Mapping集合

@Mapping的属性:

① 基本属性

  • source:映射源的属性名(对应方法参数的属性)
  • target:最终转换的目标对象的属性名(返回值对象的属性)

② 格式化

  • numberFormat():数字格式化
  • dateFormat():日期格式化

③ 其他

  • boolean ignore():是否忽略该属性
  • String defaultValue():添加默认值
  • String defaultExpression():添加默认的java表达式,只有在source的值为null时才生效
  • String expression():添加java表达式

注:在使用expression表达式时如果引入的方法,需要通过@Mapper中的import进行导入

例:

@Mapper(imports = {LocalDateTime.class})
public abstract class CarMapConvert {

    //获取当前对象
    public static  CarMapConvert INSTANCE = Mappers.getMapper(CarMapConvert.class);

    //转换方法,参数为原实体对象,返回值为结果对象,方法为转换操作

    @Mappings(value = {
            @Mapping(source ="driverDTO.name" ,target = "driverName",expression = "java"),
            @Mapping(source = "publishDate",target = "publishDate",defaultExpression = "java(LocalDateTime.now())")
    })
    public abstract CarDriverVO CarDTOToVO(CarDTO carDTO);
}

5. 映射处理

  • @BeforeMapping:在Mapper自动转换之前执行方法
  • @AfterMapping:在Mapper自动转换完成之后执行方法

它们类似于String的AOP,在某一个操作完成之前或之后进行相关处理

@MappingTarget:对转换之后的对象进行参数注入赋值,假如需要使用转换后的对象进行操作就使用该注解进行赋值

例:对CarDriverVO的partexites属性是否具体零件进行判断赋值

@Mapper
public abstract class CarConvert {

    //获取当前对象
    public static  CarConvert INSTANCE = Mappers.getMapper(CarConvert.class);

    //转换方法,参数为原实体对象,返回值为结果对象,方法为转换操作
    public abstract CarDriverVO CarDTOToVO(CarDTO carDTO);

    @AfterMapping
    public void  CarDriverVOHandler(CarDTO carDTO,@MappingTarget CarDriverVO carDriverVO){

        System.out.println("执行了后置处理方法");
        boolean par = carDTO.getPartDTOS()!=null && !carDTO.getPartDTOS().isEmpty();
        carDriverVO.setPartexites(par);
    }

}

结果:

执行了后置处理方法
CarDriverVO(id=1, vin=湘Ba8921, price=182382.52, totalPrice=200000.0, publishDate=2022-10-07, brand=日产, partexites=true, driverName=null)

6. 批量映射

6.1 传统方式进行批量转换

① 转换类

@Mapper
public abstract class CarConvert {

    //获取当前对象
    public static  CarConvert INSTANCE = Mappers.getMapper(CarConvert.class);


    //转换方法,参数为原实体对象,返回值为结果对象,方法为转换操作
    public abstract CarDriverVO CarDTOToVO(CarDTO carDTO);

}

② 批量转换

//DTO集合
List<CarDTO> carDTOList = new ArrayList<>();
carDTOList.add(getCarDto());

//VO集合
List<CarDriverVO> carDriverVOList = new ArrayList<>();

//批量转换成VO集合
for(CarDTO carDTO:carDTOList){
    CarDriverVO carConvert = CarConvert.INSTANCE.CarDTOToVO(carDTO);
    carDriverVOList.add(carConvert);
}

6.2 MapStruct批量映射

MapStruct会自动读取集合之中的泛型对象,并进行批量转换

例:

① 转换类

@Mapper
public abstract class CarConvertList {

    //获取当前对象
    public static  CarConvertList INSTANCE = Mappers.getMapper(CarConvertList.class);

    //转换方法,参数为原实体对象,返回值为结果对象,方法为转换操作
    public abstract List<CarDriverVO> CarDTOToVO(List<CarDTO> carDTO);
}

② 测试

//DTO集合
List<CarDTO> carDTOList = new ArrayList<>();
carDTOList.add(getCarDto());

List<CarDriverVO> carConvert = CarConvertList.INSTANCE.CarDTOToVO(carDTOList);

System.out.println(carConvert);

7. 映射继承配置

7.1映射继承配置

@InheritConfiguration:映射继承注解,适应于增量更新场景(继承指继承@Mapping注解)

例1:

① 转换类

@Mapper
public abstract class CarInheritConvert {

    //获取当前对象
    public static  CarInheritConvert INSTANCE = Mappers.getMapper(CarInheritConvert.class);

    //转换方法,参数为原实体对象,返回值为结果对象,方法为转换操作
    public abstract CarDriverVO CarDTOToVO(CarDTO carDTO);

    //对属性进行更新方法
    abstract public void  UpdateCarDriverVO(CarDTO carDTO,@MappingTarget CarDriverVO carDriverVO);
}

② 更新

CarDriverVO carInheritConvert =CarInheritConvert.INSTANCE.CarDTOToVO(getCarDto());

System.out.println("更新前的对象:"+carInheritConvert);
//更新对象
CarDTO carDTO = new CarDTO();
carDTO.setId(1L);
carDTO.setVin("湘A2193");
//通过UpdateCarDriverVO方法进行更新
CarInheritConvert.INSTANCE.UpdateCarDriverVO(carDTO,carInheritConvert);

//更新后的对象
System.out.println("更新后的对象:"+carInheritConvert);

结果:

更新前的对象:CarDriverVO(id=1, vin=湘Ba8921, price=182382.52, totalPrice=200000.0, publishDate=2022-10-07, brand=日产, partexites=false, driverName=null)
更新后的对象:CarDriverVO(id=1, vin=湘A2193, price=0.0, totalPrice=0.0, publishDate=null, brand=null, partexites=false, driverName=null)

更新只是做了重新映射,只有赋值的属性有值,不能继承更新前属性的值,做不到增量更新

例2:
 
① 转换类
@Mapper
public abstract class CarInheritConvert {


    //获取当前对象
    public static  CarInheritConvert INSTANCE = Mappers.getMapper(CarInheritConvert.class);


    //转换方法,参数为原实体对象,返回值为结果对象,方法为转换操作
    public abstract CarDriverVO CarDTOToVO(CarDTO carDTO);


    //对属性进行更新方法
    @InheritConfiguration
    @Mappings(value = {
            @Mapping(source = "price",target = "price",ignore = true),
            @Mapping(source = "totalPrice",target = "totalPrice",ignore = true),
    })
    abstract public void  UpdateCarDriverVO(CarDTO carDTO,@MappingTarget CarDriverVO carDriverVO);


}

② 更新

CarDriverVO carInheritConvert =CarInheritConvert.INSTANCE.CarDTOToVO(getCarDto());

System.out.println("更新前的对象:"+carInheritConvert);
//更新对象
CarDTO carDTO = new CarDTO();
carDTO.setId(1L);
carDTO.setVin("湘A2193");
//通过UpdateCarDriverVO方法进行更新
CarInheritConvert.INSTANCE.UpdateCarDriverVO(carDTO,carInheritConvert);

//更新后的对象
System.out.println("更新后的对象:"+carInheritConvert);

结果:

更新前的对象:CarDriverVO(id=1, vin=湘Ba8921, price=182382.52, totalPrice=200000.0, publishDate=2022-10-07, brand=日产, partexites=false, driverName=null)
更新后的对象:CarDriverVO(id=1, vin=湘A2193, price=182382.52, totalPrice=200000.0, publishDate=null, brand=null, partexites=false, driverName=null)

7.2 反向映射继承配置

还有另外一个类似的场景,就是编写映射函数将DTO转为VO,以及将VO转为DTO

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


    @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
    Patient toModel(PatientDto patientDto);


    @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
    PatientDto toDto(Patient patient);
}

两个方法的配置不会是完全相同的,实际上,它们应该是相反的。将DTO转为VO,以及将 VO转为DTO——映射前后的字段相同,但是源属性字段与目标属性字段是相反的

我们可以在第二个方法上使用@InheritInverseConfiguration注解,避免写两遍映射配置:

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

    @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
    Patient toModel(PatientDto patientDto);

    @InheritInverseConfiguration

    PatientDto toDto(Patient patient);
}

8. 忽略默认映射行为

@BeanMapping会忽略默认的MapStruct产生的映射,只有通过@Mapping指定的映射才会生效

例1:

① 转换类

@Mapper
public abstract class CarConvert {

    //获取当前对象
    public static  CarConvert INSTANCE = Mappers.getMapper(CarConvert.class);

    //转换方法,参数为原实体对象,返回值为结果对象,方法为转换操作
    @BeanMapping
    public abstract CarDriverVO CarDTOToVO(CarDTO carDTO);
}

② 测试

public static void main(String[] args) {

    CarDriverVO carConvert = CarConvert.INSTANCE.CarDTOToVO(getCarDto());
    System.out.println(carConvert);

}

结果为空,没有映射

9. MapStruct和Spring整合使用

之前我们一直在通过getMapper()方法访问生成的映射器:

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

但是,如果你使用的是Spring,只需要简单修改映射器配置,就可以像常规依赖项一样注入映射器。

修改 DoctorMapper 以支持Spring框架:

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

}

在@Mapper注解中添加(componentModel = "spring"),是为了告诉MapStruct,在生成映射器实现类时,我们希望它能支持通过Spring的依赖注入来创建。现在,就不需要在接口中添加 INSTANCE 字段了。

这次生成的 DoctorMapperImpl 会带有 @Component 注解:

@Component
public class DoctorMapperImpl implements DoctorMapper {
}

只要被标记为@Component,Spring就可以把它作为一个bean来处理,你就可以在其它类(如控制器)中通过@Autowire注解来使用它:

@Controllerpublic class DoctorController() {
    @Autowired
    private DoctorMapper doctorMapper;
}

如果你不使用Spring, MapStruct也支持Java CDI

@Mapper(componentModel = "cdi")
public interface DoctorMapper {

}

路漫漫其修远兮,吾将上下而求索