MapStruct 解了对象映射的毒

前言

MVC模式是目前主流项目的标准开发模式,这种模式下框架的分层结构清晰,主要分为Controller,Service,Dao。分层的结构下,各层之间的数据传输要求就会存在差别,咱们不能用一个对象来贯穿3层,这样不符合开发规范且不够灵活。前端

咱们经常会遇到层级之间字段格式需求不一致的状况,例如数据库中某个字段是datetime日期格式,这个时间戳在数据库中的存储值为2020-11-06 23:59:59.999999,可是传递给前端的时候要求接口返回yyyy-MM-dd的格式,或者有些数据在数据库中是逗号拼接的String类型,可是前端须要的是切割后的List类型等等。java

因此咱们提出了层级间的对象模型,就是咱们常见的VO,DTO,DO,PO等等。这种区分层级对象模型的方式虽然清晰化了咱们各层级间的对象传递,可是对象模型间的相互转换和值拷贝确是让人感受很麻烦,拷贝来拷贝去,来来回回,过程重复乏味,编写此类映射代码是一项繁琐且容易出错的任务。git

最简单粗糙的拷贝方法就是不断的new对象而后对象间的 setter 和 getter,这种方式应对字段属性少的还能够,若是属性字段不少那么大段的set,get的代码就显得很不雅美。所以须要借助对象拷贝工具,目前市场上的也蛮多的像BeanCopy,Dozer等等,可是这些我感受都不够好,今天我推荐一个实体映射工具就是 MapStructgithub

介绍

MapStruct的官网地址是 mapstruct.org/MapStruct,是一个快速安全的bean 映射代码生成器,只须要经过简单的注解就能够实现对象间的属性转换,是一款 Apache LICENSE 2.0 受权的开源产品,Github的源码地址是 https://github.com/mapstruct。数据库

经过官网的三连问(What,Why,How)咱们能够大概的了解到 MapStruct 的做用,它的优点以及它是如何实现的。express

从上面的三连问中咱们能够获得以下信息:安全

  • 基于约定优于配置的方法 MapStruct 极大地简化了 Java bean 类型之间的映射的实现,经过简单的注解就能够工做。生成的映射代码使用普通的方法调用而不是反射,所以速度快,类型安全且易于理解。markdown

  • 在编译时生成 Bean 映射 与其余映射框架相比,MapStruct 在编译时生成 Bean 映射,这样能够确保高性能,并且开发人员能够快速的获得反馈和完全的错误检查。app

  • 一个注释处理器 MapStruct 是一个注释处理器,已插入 Java 编译器,可用于命令行构建(Maven,Gradle等),也可用于您首选的IDE中(IDEA,Eclipse等)。框架

代码编写

MapStruct 须要 Java 1.8或更高版本。对于Maven-based 的项目,在pom 文件中添加以下依赖便可

<!-- 指定版本-->
<properties>  <org.mapstruct.version>1.4.1.Final</org.mapstruct.version> </properties> <!-- 添加依赖 --> <dependencies>  <dependency>  <groupId>org.mapstruct</groupId>  <artifactId>mapstruct</artifactId>  <version>${org.mapstruct.version}</version>  </dependency>  <dependency>  <groupId>org.mapstruct</groupId>  <artifactId>mapstruct-processor</artifactId>  <version>${org.mapstruct.version}</version>  </dependency> </dependencies> 复制代码

基本的依赖引入后就能够编写代码了,简单的定义一个映射类,为了与 Mybatis中的 mapper 接口区分,咱们能够取名为 xxObjectConverter

例如汽车对象的映射类名为 CarObjectConverter,咱们有两个对象模型 DO 和 DTO,它们内部的属性字段以下:

数据库对应的持久化对象模型 CarDo

public class Car {
 @ApiModelProperty(value = "主键id")  private Long id;   @ApiModelProperty(value = "制造商")  private String manufacturers;   @ApiModelProperty(value = "销售渠道")  private String saleChannel;   @ApiModelProperty(value = "生产日期")  private Date productionDate;  ... } 复制代码

层级间传输的对象模型 CarDto

public class CarDto {
 @ApiModelProperty(value = "主键id")  private Long id;   @ApiModelProperty(value = "制造商")  private String maker;   @ApiModelProperty(value = "销售渠道")  private List<Integer> saleChannel;   @ApiModelProperty(value = "生产日期")  private Date productionDate;  ... } 复制代码

再编写具体的 MapStruct 对象映射器

@Mapper
public interface CarObjectConverter{   CarObjectConverter INSTANCE = Mappers.getMapper(CarObjectConverter.class);   @Mapping(target = "maker", source = "manufacturers")  CarDto carToCarDto(Car car);  } 复制代码

对于字段名相同的能够不用额外的指定映射规则,可是字段名不一样的属性则须要指出字段的映射规则,如上咱们持久层 DO 的制造商的字段名是manufacturers 而层级间传输的DTO模型中则是maker,咱们就须要在映射方法上经过@Mapping注解指出映射规则,我我的习惯是喜欢将target写在前面,source写在后面,这样是与映射对象的位置保持一致,差别字段多的时候方便对比且不易混淆。

开发过程当中还会常常遇到一些日期格式的转换,就如开篇时说的那种,这时咱们也能够指定日期的映射规则

@Mapper
public interface CarObjectConverter{   CarObjectConverter INSTANCE = Mappers.getMapper(CarObjectConverter.class);   @Mapping(target = "maker", source = "manufacturers")  @Mapping(target = "productionDate", dateFormat = "yyyy-MM-dd", source = "productionDate")  CarDto carToCarDto(Car car);  } 复制代码

这些都仍是一些简单的字段的映射,但有时候咱们两个对象模型间的字段类型不一致,如上汽车的销售渠道字段saleChannel,这个在数据库中是字符串逗号拼接的值1,2,3,而咱们传递出去的须要是 List 的 Integer 类型,这种复杂的如何映射呢?

也是有方法的,咱们先编写一个将字符串逗号分隔而后转成 List 的工具方法,以下

public class CollectionUtils {
  public static List<Integer> list2String(String str) {  if (StringUtils.isNoneBlank(str)) {  return Arrays.asList(str.split(",")).stream().map(s -> Integer.valueOf(s.trim())).collect(Collectors.toList());  }  return null;  } } 复制代码

而后在映射Mapping中使用表达式便可

@Mapper
public interface CarObjectConverter {   CarObjectConverter INSTANCE = Mappers.getMapper(CarObjectConverter.class);   @Mapping(target = "maker", source = "manufacturers")  @Mapping(target = "productionDate", dateFormat = "yyyy-MM-dd", source = "productionDate")  @Mapping(target = "saleChannel", expression = "java(com.jiajian.demo.utils.CollectionUtils.list2String(car.getSaleChannel()))")  CarDto carToCarDto(Car car);  } 复制代码

这样就完成了全部字段的映射工做,咱们在须要对象模型转换的地方按照以下方式调用便可

CarDto carDto = CarObjectConverter.INSTANCE.carToCarDto(car);
复制代码

这种是单体对象之间的 Copy 不少时候咱们须要 List 对象模型间的转换,只须要再写一个方法carToCarDtos便可

@Mapper
public interface CarObjectConverter{   CarObjectConverter INSTANCE = Mappers.getMapper(CarObjectConverter.class);   @Mapping(target = "maker", source = "manufacturers")  @Mapping(target = "productionDate", dateFormat = "yyyy-MM-dd", source = "productionDate")  @Mapping(target ="saleChannel", expression = "java(com.jiajian.demo.utils.CollectionUtils.list2String(car.getSaleChannel()))")  CarDto carToCarDto(Car car);   List<CarDto> carToCarDtos(List<Car> carList);  } 复制代码

探个究竟

会不会好奇这是怎么实现的,咱们只是建立了一个接口而后在接口方法上加一个注解并在注解里面指定字段的映射规则就能够实现对象属性间的拷贝,这是怎么作到的呢?

咱们这里经过 MapStruct 建立的只是一个接口,要实现具体的功能接口必有实现。

MapStruct 会在咱们代码编译的时候为咱们建立一个实现类,而这个实现类里面经过字段的setter, getter方法来实现字段的赋值,从而实现对象的映射。

这里须要注意一点:若是你修改了任一映射对象,记得须要先执行mvn clean再启动项目,不然调试的时候会报错。

结尾

MapStrut 的功能远不至于上面介绍的这些,我只是挑出几个经常使用的语法进行示例讲解,若是读者感兴趣想深刻的了解更多能够参考官方的参考文档,Reference Guide

碰见 MapStruct 后我就开始在项目中抛弃掉了原来的那些 BeanCopyUtils 的工具,相对而言 MapStruct 确实更简洁且易使用并且定制功能也很强。

从编译文件能够看出 MapStruct 是经过setter,getter来实现属性值的拷贝,而后这种方式不是最简单又最安全高效的吗?只是 MapStruct 更好的帮助咱们实现了,避免了项目中冗余的重复代码,大道至简。

本文使用 mdnice 排版

相关文章
相关标签/搜索