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

MapStruct.png

1. 痛点

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

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 类:spring

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,一般咱们会这么写一个方法进行转换:express

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

今天就搞一搞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的映射:markdown

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映射方法讲解

上面短短几行代码就能够了十分简单!解释一下操做步骤:mybatis

首先声明一个映射接口用@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

相关文章
相关标签/搜索