工程师工具箱系列(1)MapStruct

文章目录

    • 工程师工具箱系列(1)MapStruct
      • 芸芸众生
      • 初窥门径
        • 引入POM依赖
        • 创建转换器与方法
        • 进行使用
        • IDEA好基友
      • 游刃有余
        • 示例说明
        • 避免编写重复转换器
        • 实现复杂灵活转换
    • 温故知新

工程师工具箱系列(1)MapStruct

芸芸众生

在Java项目开发中,不管你是采用传统的MVC分层模式,还是DDD驱动的微服务模式,都免不了在各层级之间传递对象,在这个过程中会出现许多的对象概念性名词:VO,DTO,DO,Entity,ValueObj等等。我们先不管这些对象在你们各自项目里的作用,有一个共同的工作就是完成他们之间赋值转换。

靠手动赋值来完成对象转换的人毕竟已经很稀缺了,我们一般都知道借助一些工具去简化这部分重复劳动。

目前市面上用的比较常见的可能有下面这几种:

它们之间的性能对比大致如下:

结合性能和吞吐量来看,手动写性能肯定是最高的,省去中间商赚差价嘛,但是社会有分工才能进步,整体效能才能增加,所以我们应该借助工具。

综合分析下来,MapStruct的性能和吞吐量都是最好的,毕竟实现原理上决定了一切,接下来我们就上手下MapStruct。

初窥门径

mapstruct的使用和如何把大象放进冰箱的步骤是一样的:1 引入mapstruct;2 创建转换器与转换方法;3 获取转换实例进行使用

引入POM依赖

Maven

<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>${org.mapstruct.version}</version>
</dependency>
...
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.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></plugins>
</build>

Gradle

plugins {...id "com.diffplug.eclipse.apt" version "3.26.0" // Only for Eclipse
}
dependencies {...compile 'org.mapstruct:mapstruct:1.4.2.Final'annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'testAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final' // if you are using mapstruct in test code
}
创建转换器与方法

创建之前你肯定已经明确了需要转换的两个类,比如下面的代码示例,是将Car对象转换成一个CarDto对象

@Mapper //指定该类为mapstruct的映射器
public interface CarMapper {// 通过ClassLoader加载CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );//转换方法,自动匹配名称与类型相同的字段,不同的字段需要通过Mapping注解进行指定// 这里就指定了将Cat对象的numberOfSeats属性转换赋值到CatDto的seatCount属性@Mapping(target = "seatCount", source = "numberOfSeats")CarDto carToCarDto(Car car);
}
进行使用

使用的时候就特别简单了,直接获取转换器的实例,调用转换方法,传入对应的参数即可

CarDto carDto = UserMapper.INSTANCE.carToCarDto(car);

好像还蛮简单的,但是实际开发的时候可没怎么简单,实际的业务和不用的开发人员有不同的习惯,有时候面临的场景就会复杂起来:

  • 字段名称相同,但是类型不同怎么处理?mapstruct会帮我们自动转换吗,它怎么知道怎么转换?
  • 对象和字符串之间转换怎么处理?实际开发
  • 列表和列表之间转换怎么处理?难道我也循环遍历吗?
  • 灵活的自定义转换怎么处理?

另外喜欢偷懒的小伙伴可能还会有一个疑问:虽然说用起来只有三步,但是每次都要为两个转换对象创建一个转换器的话,那岂不是会有很多的转换器了?

这些问题都会游刃有余小节中得到解答,该小结中利用了面向对象设计方法,省去了编写大量转换器与方法工作,并利用java8新特性方便实现灵活的自定义转换。

IDEA好基友

为了更好使用mapstruct,如果你使用的是Intellij IDEA编辑器,那么建议你安装个插件,它可以为我们提供一些遍历操作。

安装时候直接在IDEA的插件市场上搜索mapstruct,安装重启即可,插件为我们提供了一下几个便捷操作:

  • 自动填充属性与枚举常量
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 点击可以直达注解使用的声明字段
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 可以查找使用过的地方

PS:插件地址:https://plugins.jetbrains.com/plugin/10036-mapstruct-support

游刃有余

示例说明

为了更好说明示例,我们定义两个需要转换的对象类,我把它们之间字段的区别也列了出来

  • 相同字段:指的是名词和类型都相同,工具会自动转换
  • 原始类特有:指的是原始类UserE所特有的,可能有3种情况:类型一致但是名称不一致,类型不一致名称也不一致,类型不一致名称一致
  • 目标类特有:指的是目标类UserVO所特有的,它同时也对应上面原始类的三种情况

避免编写重复转换器

要避免编写重复的转换器接口,类似我们要避免编写不同类型的字段进行某种相同计算一样。很自然的就想到使用泛型来解决。

我们可以定义一个基础接口,包含了通用的映射方法,只要是字段类型相同的对象需要转换,这个基础接口就满足了,通过继承基础接口,传入具体的转换类型,无需任何实现与配置。

这里我提供了三种通用转换方法:1 单对象的转换;2 列表对象的转换;3 Stream对象转换,因为每种类型存在互相转换,所以基础接口包含了6个方法

同时,你可以把项目中约定好的一些字段约束加到其中,比如创建日期的格式等等

实现复杂灵活转换

接下来就是解决上面表格中的3种情况,它们的解决方案分别如下:

首先定义个UserMapping接口,继承BaseMapping,传入转换的类型,注意你自己规定的SOURCE和TARGET参数,不要搞混就行

@Mapper(componentModel = "spring")//spring注入方式
public interface UserMapping extends BaseMapping<UserE,UserVO>{

重载接口的方法,比如现在我们把UserE转换成为UserVO,解决类型一致,名称不一致的Mapping示例

@Mappings({@Mapping(source = "etest", target = "vtest"),@Mapping(source = "sex", target = "gender"),})@OverrideUserVO sourceToTarget(UserE var1);

去掉@Mappings,直接把多个@Mapping加在方法上面作用是相同的

那怎么解决cteateTime名称一致,类型不一致呢?

从UserE到UserVO是把时间类型转换为String类型,这是一种很常见的转换常见,注意看我们在基础接口中定义了目标字段的cteateTime时间格式,这就给工具提供了自动转换的可能性,主要给出的格式符合这个要求,那么工具会自动帮助我们完成转换

/*** 映射同名属性*/@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")TARGET sourceToTarget(SOURCE var1);

最后看名称不一致,类型也不一致的字段,UserE中的字符串如何变成UserVO中的一个对象,首先容易想到的一点是我们可以通过@Mapping配置建立二者之间的转换关系,但是工具肯定不知道怎么转换了,所以还我们需要提供如何转换方法。

@Mappings({@Mapping(source = "etest", target = "vtest"),@Mapping(source = "sex", target = "gender"),@Mapping(source = "configE",target = "configs")})@OverrideUserVO sourceToTarget(UserE var1);

那么如何提供呢?假设我们已经写好一个转换方法,应该如何告知工具去选择使用?我相信你已经想到了,只要指定入参和出参类型,再结合mapping指定映射关系工具应该就能完成转换了。

于是我们再利用java8种接口可以使用默认方法的特性,我们直接在接口里增加

/*** 映射string config 到 List<UserVO.UserConfig> list的转换* 会被自动调用*/default List<UserVO.UserConfig> strConfigToListUserConfig(String config) {return JSONUtil.toList(config,UserVO.UserConfig.class);}

这两步加起来就构成完成了类型不一致,名称不一致属性之间的转换

但是其实还是存在一个问题,如果存在多个指定转换关系,入参和出参也一致的情况,那工具就不知道具体采用哪个默认方法了。所以我们还需要知道如何完全自定义转换。
自定义一个转换类

public class CustmMapping {public static String convertFiled1(UserVO.UserConfig userConfig){return "自定义" + userConfig.getField1();}
}

在接口类中导入转换类(1处),在@Mapping中指定目标字段的转换类函数(2处)

@Mapper(componentModel = "spring",imports = CustmMapping.class)//1处
public interface UserMapping extends BaseMapping<UserE,UserVO>{
...
@Mapping(target = "sex", source = "gender")
@Mapping(target = "password", ignore = true)
@Mapping(target = "etest", source = "vtest") 
@Mapping(target="configE",expression="java(CustmMapping.convertFiled1(var1.getConfigs().get(0)))")//2处
@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Override
UserE targetToSource(UserVO var1);
...

对应的测试代码:

@Log4j2
@DisplayName("使用MapStruct进行对象赋值转换")
public class MapStructTest {private static UserE userE;private static UserVO newUserVO;private static UserMapping userMapping;@BeforeAllpublic static void init() {userE = new UserE().setId(100L).setBirthday(LocalDate.of(1988,02,25)).setUsername("临江仙").setCreateTime(LocalDateTime.now()).setSex(1).setEtest(Arrays.asList("a","b","c")).setConfigE("[{\"field1\":\"Test Field1\",\"field2\":500}]");userMapping = Mappers.getMapper(UserMapping.class);List<UserVO.UserConfig> userConfigs = new ArrayList<>();for (int i = 0; i < 5; i++) {UserVO.UserConfig userConfig = new UserVO.UserConfig("字段"+i, i+10);userConfigs.add(userConfig);}newUserVO = new UserVO().setId(200L).setUsername("鹊桥仙").setPassword("123321").setBirthday(LocalDate.of(1988,07,06)).setCreateTime("1988-02-25 12:00:00").setGender(2).setVtest(Arrays.asList("备注1","备注2","备注3")).setConfigs(userConfigs);log.info("@BeforeAll: init()");}@DisplayName("准备好UserE和UserVO")@Testpublic void testHasUserEandUserVO(){System.out.println("准备好的userE:" + userE);System.out.println("准备好的newUserVO:" + newUserVO);;}@DisplayName("将UserE转换成UserVO")@Testpublic void testEtoVO() {UserVO userVO = userMapping.sourceToTarget(userE);System.out.println("转化后得到的userVO: " + userVO);}@DisplayName("将UserVO转换成UserE")@Testpublic void testVOtoE(){UserE userE = userMapping.targetToSource(newUserVO);System.out.println("转化后得到的userE:" + userE);}
}

测试结果:

转化后得到的userVO: UserVO(id=100, username=临江仙, password=null, gender=1, birthday=1988-02-25, createTime=2021年6月1号, vtest=[a, b, c], configs=[UserVO.UserConfig(field1=Test Field1, field2=500)])转化后得到的userE:UserE(id=200, username=鹊桥仙, password=null, sex=2, birthday=1988-07-06, createTime=1988-02-25T12:00, etest=[备注1, 备注2, 备注3], configE=自定义字段0)准备好的userE:UserE(id=100, username=临江仙, password=null, sex=1, birthday=1988-02-25, createTime=2021-06-03T13:22:42.203, etest=[a, b, c], configE=[{"field1":"Test Field1","field2":500}])
准备好的newUserVO:UserVO(id=200, username=鹊桥仙, password=123321, gender=2, birthday=1988-07-06, createTime=1988-02-25 12:00:00, vtest=[备注1, 备注2, 备注3], configs=[UserVO.UserConfig(field1=字段0, field2=10), UserVO.UserConfig(field1=字段1, field2=11), UserVO.UserConfig(field1=字段2, field2=12), UserVO.UserConfig(field1=字段3, field2=13), UserVO.UserConfig(field1=字段4, field2=14)])

温故知新

最后我们对mapstruct工具做个小结:

  • 核心特点 :基于 JSR 269 的 Java 注解处理器实现,用纯java方法而不是反射进行属性赋值,做到了编译时类型安全,相当于编译时的代码生成器。

  • 性能更高:使用简单的Java方法调用代替反射,无需手动 set/get 或 implements Serializable 以达到深拷贝
  • 编译时类型安全:只能映射相同名称或带映射标记的属性,编译时如果映射不完整(存在未被映射的目标属性)或映射不正确(找不到合适的映射方法或类型转换)则会在编译时抛出异常

使用技巧

  • 技巧一:定义一个公共的转换器接口,使用泛型定义好常用的方法,如果字段完全一样公共接口就满足要求了
  • 技巧二:同类型不同名称的转换直接使用Mapping在转换方法上指定
  • 技巧三:不同类型同名称的,可以使用Mapping也可以使用default方法的方式
  • 技巧四:不同类型不同名称,可以使用Mapping+default方式或自定义转换类方式

运用这些技巧你还可以实现多个bean之间映射,复杂数据结构之间映射等,充分满足多种业务场景下使用。

PS:文中源码是示例地址:https://gitee.com/hzqiuxm/middleware-projects.git [java-base模块]-[mapstruct包]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/691594.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【全开源】Java俱乐部系统社区论坛商城系统源码-奔驰奥迪保时捷大众宝马等汽车俱乐部

特色功能&#xff1a; 会员中心&#xff1a;会员中心可以帮助企业更好地管理客户&#xff0c;包括设置积分商城、会员卡充值、个人汽车档案等功能&#xff0c;对不同的会员群体展开有针对性的营销&#xff0c;并维护和积累自己的粉丝群体。信息服务&#xff1a;负责定期发布新…

【雷音系·雷修】倪琴古琴,倪诗韵亲签古琴

雷音系列雷修&#xff1a;“修”字取意善、美好的&#xff0c;更有“使之完美”之意。精品桐木或普通杉木制&#xff0c;栗壳色&#xff0c;纯鹿角霜生漆工艺。方形龙池凤沼。红木配件&#xff0c;龙池上方有“倪诗韵”亲笔签名&#xff0c;凤沼下方位置处有“雷•修”等级葫芦…

谷歌最强AI——Gemini免费使用2个月教程,性能抗衡GPT4

谷歌最强AI——Gemini采用的是Ultra 1.0大模型&#xff0c;功能非常强大&#xff0c;媲美GPT-4&#xff01;谷歌用户只需要绑定虚拟卡&#xff0c;就可以免费使用2个月&#xff01; 谷歌昨夜官宣四项AI新进展&#xff01; 1、最大、功能最强的大模型版本Gemini Ultra 1.0全面…

动态规划算法练习——计数问题

题目描述 给定两个整数 a 和 b&#xff0c;求 a 和 b 之间的所有数字中 0∼9 的出现次数。 例如&#xff0c;a1024&#xff0c;b1032&#xff0c;则 a 和 b 之间共有 9 个数如下&#xff1a; 1024 1025 1026 1027 1028 1029 1030 1031 1032 其中 0 出现 10 次&#xff0c;1 出现…

FreeRTOS的列表和列表项 list.c文件详解

列表、列表项的定义以及初始化 列表相当于链表&#xff0c;列表项相当于节点&#xff0c;FreeRTOS中的列表相当于一个双向环形链表。 列表使用指针指向列表项。一个列表&#xff08;list&#xff09;下面可能有很多个列表项&#xff08;list item&#xff09;&#xff0c;每个…

【MySQL】基本操作

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;折纸花满衣 &#x1f3e0;个人专栏&#xff1a;MySQL 目录 &#x1f449;&#x1f3fb;创建和删除数据库&#x1f449;&#x1f3fb;数据库编码集和数据库校验集校验规则对数据库的影响 &#x1f449;&…

泰迪智能科技大数据开发实训平台功能介绍

大数据开发实训平台是面向实训课和课后训练的编程实训平台&#xff0c;平台底层基于Docker技术&#xff0c;采用容器云部署方案&#xff0c;预装大数据相关课程教学所需的实训环境&#xff0c;拥有1主2从的Hadoop集群&#xff0c;还能够自主定制环境&#xff0c;并能够与实训管…

SSM【Spring SpringMVC Mybatis】—— Spring(一)

目录 1、初识Spring 1.1 Spring简介 1.2 搭建Spring框架步骤 1.3 Spring特性 1.5 bean标签详解 2、SpringIOC底层实现 2.1 BeanFactory与ApplicationContexet 2.2 图解IOC类的结构 3、Spring依赖注入数值问题【重点】 3.1 字面量数值 3.2 CDATA区 3.3 外部已声明be…

JeeSite V5.7.0 发布,Java快速开发平台,Vite5、多项重构重磅升级

JeeSite V5.7.0 发布&#xff0c;Java快速开发平台&#xff0c;Vite5、多项重构重磅升级 升级内容 新增 参数配置 IP 地址黑白名单过滤器动态参数 新增 侧边栏是否展开第一个菜单的开关 first-open 新增 AesTypeHandler 处理字段数据加密解密或脱敏 新增 JsonTypeHandler …

PCIE协议-2-事务层规范-Message Request Rules

2.2.8 消息请求规则 本文档定义了以下几组消息&#xff1a; INTx 中断信号电源管理错误信号锁定事务支持插槽电源限制支持厂商定义消息延迟容忍度报告&#xff08;LTR&#xff09;消息优化缓冲区冲洗/填充&#xff08;OBFF&#xff09;消息设备就绪状态&#xff08;DRS&#…

机器人系统ros2-开发实践06-将静态坐标系广播到 tf2(Python)-定义机器人底座与其传感器或非移动部件之间的关系

发布静态变换对于定义机器人底座与其传感器或非移动部件之间的关系非常有用。例如&#xff0c;最容易推断激光扫描仪中心框架中的激光扫描测量结果。 1. 创建包 首先&#xff0c;我们将创建一个用于本教程和后续教程的包。调用的包learning_tf2_py将依赖于geometry_msgs、pyth…

EasyRecovery(易恢复) 使用测试及详细使用方法

你有没有因为数据丢失懊悔不已&#xff0c;EasyRecovery&#xff08;易恢复&#xff09;&#xff0c;来自美国拥有38年数据恢复的软件&#xff0c;只有收费版&#xff0c;重要事情说三遍&#xff0c;EasyRecovery 没有免费版&#xff0c;可以成功找回删除的部分文件&#xff0c…