• Spring Boot 2 实战:集成 MapStruct 类型转换神器


    在这里插入图片描述

    1. 痛点

    一种框架的出现都要解决个痛点,我想下面这这种不方便的操作经常有人写吧。
    假如Car类是数据库映射类:
    ​​

     package cn.felord.mapstruct.entity;
     
     import lombok.Data;
     
     /**
      * Car
      *
      * @author Felordcn
      * @since 13:35 2019/10/12
      **/
     @Data
     public class Car {
         private String make;
         private int numberOfSeats;
         private CarType type;
     
     }
    

    CarType 类:

     package cn.felord.mapstruct.entity;
     
     import lombok.Data;
     
     /**
      * CarType
      *
      * @author Felordcn
      * @since 13:36 2019/10/12
      **/
     @Data
     public class CarType {
         private String type;
     }
    

    ​​
    CarDTO是DTO类:

     package cn.felord.mapstruct.entity;
     
     import lombok.Data;
     
     /**
      * CarDTO
      *
      * @author Felordcn
      * @since 13:37 2019/10/12
      **/
     @Data
     public class CarDTO {
         private String make;
         private int seatCount;
         private String type;
     } 
    

    ​​
    我们从数据库查询Car 然后需要转换为CarDTO,通常我们会这么写一个方法进行转换:

         public CarDTO carToCarDTO(Car car) {
             CarDTO carDTO = new CarDTO();
             
             carDTO.setMake(car.getMake());
             carDTO.setSeatCount(car.getNumberOfSeats());
             carDTO.setType(car.getCarType().getType());
             // 有可能更长 
             return carDTO;
         } 
    

    ​​这种写法非常繁琐无味,而且没有技术含量。甚至中间还牵涉了很多类型转换,嵌套之类的繁琐操作,而我们想要的只是建立它们之间的映射关系而已。有没有一种通用的映射工具来帮我们搞定这一切。当然有而且还不少。有人说apache的BeanUtil.copyProperties可以实现,但是性能差而且容易出异常,很多规范严禁使用这种途径。以下是对几种对象映射框架的对比,大多数情况下 MapStruct 性能最高。原理类似于lombokMapStruct都是在编译期进行实现,而且基于GetterSetter,没有使用反射所以一般不存在运行时性能问题。
    ​​
    diff.png

    今天就搞一搞MapStruct, 并跟Spring Boot 2.x 集成以下。 无论是idea 还是eclipse 都建议安装 MapStruct Plugin 插件,当然不安装也是可以的。

    2. Spring Boot 2.1.9 集成 MapStruct

    在 Spring Boot 的 pom.xml 下引入 MapStruct 的 maven 依赖坐标:

             <dependencies>
                <dependency>
                     <groupId>org.mapstruct</groupId>
                     <artifactId>mapstruct</artifactId>
                     <version>${mapstruct.version}</version>
                     <scope>compile</scope>
                 </dependency>
                 <dependency>
                     <groupId>org.mapstruct</groupId>
                     <artifactId>mapstruct-processor</artifactId>
                     <version>${mapstruct.version}</version>
                     <scope>compile</scope>
                 </dependency>
                       <!-- other dependencies -->
             </dependencies>
    

    ​​

    3. 使用MapStruct

    我们把开始的痛点解决一下。看看 MapStruct 如何降低你的编程成本。

    3.1 编写转换源到目标的映射

    编写CarCarDTO的映射:

     package cn.felord.mapstruct.mapping;
     
     import cn.felord.mapstruct.entity.Car;
     import cn.felord.mapstruct.entity.CarDTO;
     import org.mapstruct.Mapper;
     import org.mapstruct.Mapping;
     import org.mapstruct.factory.Mappers;
     
     /**
      * CarMapping
      *
      * @author Felordcn
      * @since 14 :02 2019/10/12
      */
     @Mapper
     public interface CarMapping {
         /**
          * 用来调用实例 实际开发中可使用注入Spring  不写
          */
         CarMapping CAR_MAPPING = Mappers.getMapper(CarMapping.class);
     
     
         /**
          *  源类型 目标类型 成员变量相同类型 相同变量名 不用写{@link org.mapstruct.Mapping}来映射
          *
          * @param car the car
          * @return the car dto
          */
         @Mapping(target = "type", source = "carType.type")
         @Mapping(target = "seatCount", source = "numberOfSeats")
         CarDTO carToCarDTO(Car car);
     
     }
    
    

    3.2 MapStruct映射方法讲解

    上面短短几行代码就可以了十分简单!解释一下操作步骤:

    首先声明一个映射接口用@org.mapstruct.Mapper (不要跟mybatis注解混淆)标记,说明这是一个实体类型转换接口。这里我们声明了一个 CAR_MAPPING 来方便我们调用,CarDTO toCarDTO(Car car)是不是很熟悉, 像mybatis一样抽象出我们的转换方法。@org.mapstruct.Mapping注解用来声明成员属性的映射。该注解有两个重要的属性:

    • source 代表转换的源。这里就是Car
    • target 代表转换的目标。这里是CarDTO

    这里以成员变量的参数名为依据,如果有嵌套比如 Car 里面有个 CarType 类型的成员变量 carType,其 type 属性 来映射 CarDTO 中的 type 字符串,我们使用 type.type 来获取属性值。如果有多层以此类推。MapStruct 最终调用的是 settergetter 方法,而非反射。这也是其性能比较好的原因之一。numberOfSeats 映射到 seatCount 就比较好理解了。我们是不是忘记了一个属性 make,因为他们的位置且名称完全一致,所以可以省略。而且对于包装类是自动拆箱封箱操作的,并且是线程安全的。MapStruct不单单有这些功能,还有其他一些复杂的功能:

    设置转换默认值和常量。当目标值是 null 时我们可以设置其默认值,注意这些都是基本类型以及对应都 boxing 类型,如下

     @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
    

    需要注意的是常量不能对源进行引用(不能指定 source 属性),下面是正确的操作:

     @Mapping(target = "stringConstant", constant = "Constant Value")
    

    3.2 Mapper 编译

    当你的应用编译后。你会在编译后的目录比如 maven是 targetgenerated-sourcesannotations 下的子目录发现生成了一个实现类 比如 我们上面的CarMapping 会生成CarMappingImpl 如下:

     package cn.felord.mapstruct.mapping;
      
     import cn.felord.mapstruct.entity.Car;
     import cn.felord.mapstruct.entity.CarDTO;
     import org.mapstruct.Mapper;
     import org.mapstruct.Mapping;
     import org.mapstruct.factory.Mappers;
     import javax.annotation.Generated;
     import org.springframework.stereotype.Component;
     
     @Generated(
         value = "org.mapstruct.ap.MappingProcessor",
         date = "2019-10-12T15:05:36 0800",
         comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_222 (Amazon.com Inc.)"
     )
     @Component
     public class CarMappingImpl implements CarMapping {
     
         @Override
         public CarDTO carToCarDTO(Car car) {
             if ( car == null ) {
                 return null;
             }
     
             CarDTO carDTO = new CarDTO();
     
             carDTO.setType( carCarTypeType( car ) );
             carDTO.setSeatCount( car.getNumberOfSeats() );
             carDTO.setMake( car.getMake() );
     
             return carDTO;
         }
     
         private String carCarTypeType(Car car) {
             if ( car == null ) {
                 return null;
             }
             CarType carType = car.getCarType();
             if ( carType == null ) {
                 return null;
             }
             String type = carType.getType();
             if ( type == null ) {
                 return null;
             }
             return type;
         }
     }
    

    4. MapStruct 进阶操作

    下面介绍几种 MapStruct 的进阶操作:

    4.1 格式化操作

    格式化也是我们经常使用的操作,比如数字格式化,日期格式化。
    这是处理数字格式化的操作,遵循java.text.DecimalFormat的规范:

         @Mapping(source = "price", numberFormat = "$#.00")
    

    下面展示了将一个日期集合映射到日期字符串集合的格式化操作上:

     @IterableMapping(dateFormat = "dd.MM.yyyy")
     List<String> stringListToDateList(List<Date> dates);
    

    4.2 使用 java 表达式

    下面演示如何使用LocalDateTime 作为当前的时间值注入 addTime 属性中。

    首先在@org.mapstruct.Mapperimports 属性中导入 LocalDateTime,该属性是数组意味着你可以根据需要导入更多的处理类:

     @Mapper(imports = {LocalDateTime.class})
    

    接下来只需要在对应的方法上添加注解@org.mapstruct.Mapping ,其属性expression 接收一个 java() 包括的表达式:

    • 无入参版本:
     @Mapping(target = "addTime", expression = "java(LocalDateTime.now())")
    
    • 携带入参的版本, 我们将 Car 的出厂日期字符串manufactureDateStr 注入到 CarDTOLocalDateTime 类型属性addTime 中去:
     @Mapping(target = "addTime", expression = "java(LocalDateTime.parse(car.manufactureDateStr))")
     CarDTO carToCarDTO(Car car);
    

    4.3 MapStruct 转换 Mapper 注入Spring IoC 容器

    如果使用要把Mapper 注入Spring IoC 容器我们只需要这么声明,不用再构建一个单例,就可以像其他 spring bean一样对CarMapping 进行引用了:

      package cn.felord.mapstruct.mapping;
      
      import cn.felord.mapstruct.entity.Car;
      import cn.felord.mapstruct.entity.CarDTO;
      import org.mapstruct.Mapper;
      import org.mapstruct.Mapping;
      import org.mapstruct.factory.Mappers;
     
     /**
      * CarMapping 注入spring 写法
      *
      * @author Felordcn
      * @since 14 :02 2019/10/12
      */
     @Mapper(componentModel = "spring")
     public interface CarMapping {
         /**
          * 用来调用实例 实际开发中可使用注入Spring  不写
          */
     //    CarMapping CAR_MAPPING = Mappers.getMapper(CarMapping.class);
     
     
         /**
          * 源类型 目标类型 成员变量相同类型 相同变量名 不用写{@link Mapping}来映射
          *
          * @param car the car
          * @return the car dto
          */
         @Mapping(target = "type", source = "carType.type")
         @Mapping(target = "seatCount", source = "numberOfSeats")
         CarDTO carToCarDTO(Car car);
     
     }
    
    

    ​​

    5.总结

    其实MapStruct 还有很多的功能。但是从可读性来说,我建议使用以上几种容易理解的功能即可。如果你感兴趣可以去mapstruct.org进一步学习。配合lombok和我介绍的jsr303,让你更加专注于业务,而且代码更加清晰。

    关注公众号:Felordcn获取更多资讯

    个人博客:https://felord.cn

  • 相关阅读:
    堆排序
    jdk8 永久代变更
    oracle 区分大小写遇到的坑
    日志统计分析
    zookeeper 服务挂掉重启后,dubbo 服务是不会自动重新注册上的
    代码质量管理
    快速排序算法
    python flask 项目结构
    项目架构
    JS中的循环---最全的循环总结
  • 原文地址:https://www.cnblogs.com/felordcn/p/12142554.html
Copyright © 2020-2023  润新知