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是 target\generated-sources\annotations 下的子目录发现生成了一个实现类 比如 我们上面的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,让你更加专注于业务,而且代码更加清晰。

关注公众号:码农小胖哥,获取更多资讯

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容