您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
MapStruct实体映射工具
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
MapStruct实体映射工具
自猿其说Tech
2021-12-28
IP归属:未知
208960浏览
计算机编程
### 1 何为实体映射 当前我们的后端服务架构中,MVC模式大行其道。MVC即Model(模型)、View(视图)和Controller(控制),通过模型、视图与控制器的分离,能够使模块之间解耦,使得代码可以实现高度复用。于是数据传输对象(Data Transfer Object,DTO)出现了,它的出现是为了对领域对象进行数据封装,实现层与层之间的数据传递,在不同的层次之间进行数据传输的时候,需要把一个层次的DTO转换为下一个层次的DTO,这个过程就是实体映射。 ### 2 现在是怎么做的 日常开发中,我们会经常涉及PO转VO或者是VO转DTO相关的逻辑,我们通常有两种做法: #### 2.1 封装转换工具类 ```java public static GridSowedBo convert(GridSowedPo po) { GridSowedBo gridSowedBo = new GridSowedBo(); gridSowedBo.setId(po.getId()); gridSowedBo.setSendTime(po.getSendTime()); gridSowedBo.setStatus(po.getStatus()); gridSowedBo.setErrCode(po.getErrCode()); gridSowedBo.setErrStep(po.getErrStep()); gridSowedBo.setErrNodes(po.getErrNodes()); gridSowedBo.setRawData(po.getRawData()); gridSowedBo.setUpdateUser(po.getUpdateUser()); gridSowedBo.setUpdateUserName(po.getUpdateUserName()); gridSowedBo.setUpdateTime(po.getUpdateTime()); gridSowedBo.setGroupOrderCode(po.getGroupOrderCode()); gridSowedBo.setWaybillCode(po.getWaybillCode()); gridSowedBo.setTotalSowedNum(po.getTotalSowedNum()); gridSowedBo.setSource(po.getSource()); gridSowedBo.setIndexNo(po.getIndexNo()); gridSowedBo.setSowedNum(po.getSowedNum()); gridSowedBo.setBusinessTime(po.getBusinessTime()); return gridSowedBo; } ``` 是不是看着都头大?编写这种代码既耗时,又没有什么营养,但是又不能不写,而且字段变更的时候还要同步维护这些代码。 #### 2.2 通过bean拷贝工具 Spring和Apache给我们提供了BeatUtils工具了,可以通过这些工具实现实体映射 ```java GridSowedBo gridSowedBo = new GridSowedBo(); BeanUtils.copyProperties(po, gridSowedBo); ``` 看似很美好,然而这种方式也有几个问题: 1. 首先就是只能支持同名字段的映射,如果两个层次中对同一个数据字段的命名不同,就无能为力了; 1. 其次是不支持深拷贝,如果数据对象是一个复杂的实体,里面嵌套了多层对象,也不能支持映射; 1. 最后就是效率问题,bean拷贝是通过反射实现的,所以执行效率相比于第一种方式慢了很多。 ### 3 MapStruct简介 MapStruct就是为了解决实体映射的这些问题而诞生的。MapStruct是一个开源的基于Java的代码生成器,用于创建实现Java Bean之间转换的扩展映射器。使用MapStruct,我们只需要创建接口,而该工具会通过注解在编译过程中自动创建具体的映射实现,大大减少了通常需要手工编写的样板代码的数量。简而言之,我们希望实体映射采用第一种方式,但是又不希望编写这么没有营养的代码,那么MapStruct帮我们自动生成了这些代码。 ### 4 MapStruct基本原理 我们先重温下java的编译过程:Java源代码通过编译器变成了jvm可执行的Java字节码(即虚拟指令),java程序运行时jvm读取字节码通过jvm中解释器变成机器可执行的二进制机器码最终执行。其实java编译器提供了一套完整的api,我们使用接口可以方便地进行动态编译。MapStruct本质上就是一个实现了JSR 269 API的程序。在使用javac的过程中,它产生作用的具体流程如下: ![](//img1.jcloudcs.com/developer.jdcloud.com/71bd5f3e-040c-48cc-8601-bd2ffdc4bcce20211228141443.png) 首先我们看下整个框架代码的组成部分,主要分为两个包: - org.mapstruct:mapstruct: 包含了必要的注解,例如@Mapping; - org.mapstruct:mapstruct-processor: 包含生成映射器实现的注解处理器。这个就是整个MapStruct的入口,继承了注解处理器,在编译时将会调用process做操作。 注解处理器以及框架自带的处理类 都以java SPI的方式使用ServiceClass加载进来了,主要实现方法在MappingProcessor#getProcessors中。这里是用ServiceClassLoader去加载所有定义好的Process类,形成责任链的一种方式。 ![](//img1.jcloudcs.com/developer.jdcloud.com/760b515d-3fb1-4258-83b1-63be0dc40b7720211228141518.png) ### 5 MapStruct使用 #### 5.1 引入MapStruct 本文仅给出通过maven方式引入MapStruct,有兴趣的同学可以自行研究如何通过Gradle引入。 首先需要引入依赖包: ```java <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> ``` 由于MapStruct在编译时工作,并且会集成到Maven上,我们还必须在<build>标签中添加一个插件,并在其配置中添加annotationProcessorPaths,该插件会在构建时生成对应的代码: ```java <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> ``` ### 5.2 一个简单的例子 配置好maven依赖之后就可以使用MapStruct了,只需要定义一个转换接口并使用@Mapper注解,然后在接口中定义转换方法就可以了,下面是一个简单的例子: ```java @Mapper public interface GridSowedMapstruct { GridSowedMapstruct INSTANCE = Mappers.getMapper(GridSowedMapstruct.class); GridSowedPo toPo(GridSowedBo bo); } ``` 那么如何使用呢?只需要在转换的时候调用Mappers.getMapper(Class<T> clazz)方法就能获取到转换接口的实现类的实例了,然后调用需要的方法就可以了: ```java GridSowedBo gridSowedBo = GridSowedMapstruct.INSTANCE.toBo(po); ``` 是不是很简单呢?那么通过MapStruct生成的代码是什么样子呢,在编译之后我们可以在target目录下找到生成的代码: ![](//img1.jcloudcs.com/developer.jdcloud.com/02e26a51-b611-4f4c-a9af-8c1cd4e2779420211228142031.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/278fe83e-2da0-4f64-823a-a661f684440b20211228142042.png) #### 5.3 自定义映射 那么如果映射前后的字段名不相同,或者我们想自由定义某一个字段值为一个表达式的值,应该怎么办呢?MapStruct支持通过在方法上使用@Mappings注解和@Mapping注解为该映射方法自定义一系列映射规则,请看一个例子: ```java public interface GridSendMapstruct { GridSendMapstruct INSTANCE = Mappers.getMapper(GridSendMapstruct.class); @Mappings({ @Mapping(source = "operateTime", target = "businessTime") @Mapping(target = "statusStr", expression = "java(com.zhongyouex.financial.front.common.enums.fee.SubmitBillStatusEnum.getName(po.getStatus()))"), @Mapping(target = "sourceStr", expression = "java(com.zhongyouex.financial.front.common.enums.fee.SourceTypeEnum.getName(po.getSource()))"), @Mapping(target = "errMsg", expression = "java(com.zhongyouex.financial.front.common.enums.fee.FailReasonEnum.getName(po.getErrCode()))"), }) GridSendBo toBo(GridSendPo po); } ``` 我们来看一下生成的代码: ![](//img1.jcloudcs.com/developer.jdcloud.com/5c0a0094-3b70-4b4a-b1cf-65ae7aaf068c20211228142118.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/fab02edd-eacd-4578-8947-8110dfdeb00320211228142127.png) 生成的规则正是我们配置到方法上的规则。 #### 5.4 嵌套映射 那么如果有嵌套的对象应该怎么做呢?其实只要在接口中定义了嵌套对象的转换规则,MapStruct会自动使用这个规则做转换,不需要我们做额外的配置。请看这个例子: ```java @Mapper public interface GridSendMapstruct { GridSendMapstruct INSTANCE = Mappers.getMapper(GridSendMapstruct.class); GridSendPo toPo(GridSendBo bo); List<HeavySubsidyDetailPo> toSubPos(List<HeavySubsidyDetailBo> bos); } ``` 在GridSendPo中定义了一个类型为List<HeavySubsidyDetailPo>的属性,在GridSendBo中定义了一个类型为List<HeavySubsidyDetailBo>的属性。 我们看一下生成的代码: ![](//img1.jcloudcs.com/developer.jdcloud.com/22abae73-2648-4a8b-8cb2-a3471780f6a720211228142350.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/a38b71c8-02e0-468f-b970-d676c08aa8c420211228142400.png) MapStruct不仅自动转换嵌套对象,还自动生成了需要使用到的HeavySubsidyDetailBo转换成HeavySubsidyDetailPo的方法。 那如果为了代码整洁或其他原因,这两个转换方法不定义同一个接口中怎么办呢?只需要在@Mapper中声明需要引入的其他接口即可,代码如下: ```java @Mapper(uses = {HeavySubsidyMapstruct.class}) public interface GridSendMapstruct { GridSendMapstruct INSTANCE = Mappers.getMapper(GridSendMapstruct.class); GridSendPo toPo(GridSendBo bo); } @Mapper public interface HeavySubsidyMapstruct { HeavySubsidyMapstruct INSTANCE = Mappers.getMapper(HeavySubsidyMapstruct.class); List<HeavySubsidyDetailPo> toPos(List<HeavySubsidyDetailBo> bos); } ``` #### 5.5 映射到已有对象 从以上的代码中可以看到,MapStruct映射的时候,映射的对象都是新生成的对象,那么如果我们想把一个对象的属性映射到另一个已有的对象中怎么办呢?@MappingTarget注解可以帮助我们实现: ```java @Mapper public interface GridSendMapstruct { GridSendMapstruct INSTANCE = Mappers.getMapper(GridSendMapstruct.class); GridSendBo toBo(WaybillBo waybill, @MappingTarget GridSendBo gridSendBo); } ``` 生成的代码: ![](//img1.jcloudcs.com/developer.jdcloud.com/53df0a14-9b0f-4574-af98-c7f616ee10c020211228142455.png) 这里需要注意的是如果被映射对象为null,那么方法返回的也是null而不是入参中的target对象。 以上是MapStruct的一些常用的用法,有兴趣的同学可以研究一下MapStruct的一些更高级的功能用法。 ### 6 转换效率分析 我们已经介绍了MapStruct的一些基本用法,那么MapStruct的运行效率究竟怎么样呢,我们就来简单的测试一下: 这里我们对比了MapStruct和bean拷贝两种方式转换同一个对象的用时,其中bean拷贝采用了缓存反射对象以加快运行效率: ![](//img1.jcloudcs.com/developer.jdcloud.com/03cde345-f657-4338-9568-4374b235280520211228142515.png) 运行时间单位为纳秒。可以看出MapStruct运行效率是Bean拷贝运行效率的10-50倍,随着类中属性的增加和映射个数的增加,MapStruct的耗时增长较bean拷贝要快,但是其运行效率仍然比bean拷贝要高出很多。 参考文档:https://mapstruct.org/ ------------ ###### 自猿其说Tech-京东物流技术发展部 ###### 作者:何红呈(攀登者小组)
原创文章,需联系作者,授权转载
上一篇:一种实现Spring动态数据源切换的方法
下一篇:iOS synchronized底层原理分析
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号