工程师工具箱系列(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"),
    })
    @Override
    UserVO 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")
    })
    @Override
    UserVO 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;

    @BeforeAll
    public 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")
    @Test
    public void testHasUserEandUserVO(){
        System.out.println("准备好的userE:" + userE);
        System.out.println("准备好的newUserVO:" + newUserVO);;
    }
    @DisplayName("将UserE转换成UserVO")
    @Test
    public void testEtoVO() {
        UserVO userVO = userMapping.sourceToTarget(userE);
        System.out.println("转化后得到的userVO: " + userVO);
    }
    @DisplayName("将UserVO转换成UserE")
    @Test
    public 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包]

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringAI 技术解析

1. 发展历史 SpringAI 的发展历史可以追溯到对 Spring 框架的扩展和改进&#xff0c;以支持人工智能相关的功能。随着人工智能技术的快速发展&#xff0c;SpringAI 逐渐成为 Spring 生态系统中的一个重要组成部分&#xff0c;为开发者提供了便捷、灵活的解决方案。 项目的灵感来…

算法提高之单词接龙

算法提高之单词接龙 核心思想&#xff1a;dfs 预处理每两个字符串之间最短的公共部分长度 求最短公共 最终字符串是最长 dfs所有开头字符串 #include <iostream>#include <cstring>#include <algorithm>using namespace std;const int N 25;int g[N][N…

雷军-2022.8小米创业思考-6-互联网七字诀之快:天下武功,唯快不破;快不是目的,快是手段;不要用战术上的勤奋掩盖战略上的懒惰。

第六章 互联网七字诀 专注、极致、口碑、快&#xff0c;这就是我总结的互联网七字诀&#xff0c;也是我对互联网思维的高度概括。 快 我们期待非常多的快&#xff0c;比如研发进展快、库存周转快、资金回笼快等等。但在这里&#xff0c;我们集中讨论的是公司在业务发展和面对…

LeetCode题练习与总结:二叉树的中序遍历--94

一、题目描述 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2]示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[]示例 3&#xff1a; 输入&#xff1a;roo…

C语言(指针)5

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸各位能阅读我的文章&#xff0c;诚请评论指点&#xff0c;关注收藏&#xff0c;欢迎欢迎~~ &#x1f4a5;个人主页&#xff1a;小羊在奋斗 &#x1f4a5;所属专栏&#xff1a;C语言 本系列文章为个人学习笔记&#x…

Busybox 在 Docker 中的部署和启动

可以使用 docker pull 指令下载 busybox:latest 镜像&#xff1a; PS C:\Users\yhu> docker pull busybox:latest latest: Pulling from library/busybox ec562eabd705: Pull complete Digest: sha256:5eef5ed34e1e1ff0a4ae850395cbf665c4de6b4b83a32a0bc7bcb998e24e7bbb St…

COX回归特征筛选

任务&#xff1a;利用cox筛选出P值小于0.05的特征 数据的格式第一列为标签&#xff0c;第二列为时间&#xff0c;第三列及后为特征 先想一想&#xff0c;想好了再更新 这里我们先举一个例子&#xff1a; import pandas as pd from lifelines import CoxPHFitter# 创建示例数…

项目管理-计算题公式【复习】

1.【进度】相关公式 1.1三点估算 PERT 三点估算法是基于 任务成本的三种估算值&#xff08;最可能成本CM&#xff0c;最乐观成本CO&#xff0c;最悲观成本CP&#xff09;来计算预期成本的方法。 三角 分布&#xff1a;预期成本&#xff08;最乐观成本最可能成本最悲观成本&am…

RabbitMq出现Not management user问题解决

在RabbitMq登录的时候突然弹出如下图&#xff1a; 提示“当前用户不是管理员用户”进入mq控制命令台下&#xff1a; windows版本在mq安装路径下的sbin下进入cmd弹出框&#xff1b; Linux版本没有测试&#xff1b; 输入以下命令&#xff1a; rabbitmqctl list_users 查询当…

【计算机网络篇】数据链路层(8)共享式以太网的退避算法和信道利用率

文章目录 &#x1f6f8;共享式以太网的退避算法&#x1f95a;截断二进制指数算法 &#x1f354;共享式以太网的信道利用率 &#x1f6f8;共享式以太网的退避算法 在使用CSMA/CD协议的共享总线以太网中&#xff0c;正在发送帧的站点一边发送帧一边检测碰撞&#xff0c;当检测到…

Springboot整合 Spring Cloud Alibaba Sentinel

1.Sentinel介绍 官方文档地址&#xff1a; https://sentinelguard.io/zh-cn/docs/introduction.html https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件&#xff0c;主要以流量为切入…

探索共享内存:解锁并发编程的潜力

文章目录 序言shm 原理对shm的理解通过代码认识shm调用shmget方法实现 序言 system V版本 指定的一种规则(俗话说一流公司定规则,二流公司重服务,三流公司重技术).这个规则虽然有很多种(消息队列,共享内存等只是比较出名的几个).但是在内核的相关技术解决上是类似的,因为都是基…

吴恩达 深度学习 神经网络 softmax adam 交叉验证

神经网络中的层&#xff1a;输入层&#xff08;layer 0&#xff09;、隐藏层、卷积层&#xff08;看情况用这个&#xff09;、输出层。&#xff08;参考文章&#xff09; 激活函数&#xff1a; 隐藏层一般用relu函数&#xff1b; 输出层根据需要&#xff0c;二分类用sigmoid&…

Selenium 自动化 —— 一篇文章彻底搞懂XPath

更多关于Selenium的知识请访问“兰亭序咖啡”的专栏&#xff1a;专栏《Selenium 从入门到精通》 文章目录 前言 一、什么是xpath&#xff1f; 二、XPath 节点 三. 节点的关系 1. 父&#xff08;Parent&#xff09; 2. 子&#xff08;Children&#xff09; 3. 同胞&#xff08;S…

[Algorithm][回溯][全排列][子集] + 回溯原理 详细讲解

目录 0.原理讲解1.全排列1.题目链接2.算法原理详解3.代码实现 2.子集1.题目链接2.算法原理详解3.代码实现 0.原理讲解 回溯算法通常⽤于解决组合问题、排列问题和搜索问题等回溯算法的基本思想&#xff1a; 从⼀个初始状态开始&#xff0c;按照⼀定的规则向前搜索&#xff0c;…

怎么下载抖音直播视频 怎么解析直播间链接的视频录制保存

尊敬的读者们&#xff0c;你们好&#xff01;今天我们将探讨一个非常实用的技巧——如何下载直播视频。随着网络技术的发展&#xff0c;直播视频已经成为我们日常生活中不可或缺的一部分。无论是观看比赛、欣赏音乐会还是探索新的美食&#xff0c;直播视频都为我们提供了更直观…

【qt】最快的开发界面效率——混合编程

混合编程 一.准备工作1.创建项目2.添加项目资源 二.ui界面设计1.menuBar菜单栏2.action ▲3.toolBar工具栏4.中心组件 三.代码界面设计1.toolBar添加组件2.statusBar状态栏添加组件 四.完成界面的功能1.对action配置信号槽2.对action转到信号槽3.代码添加的组件手动关联槽函数 …

YOLOv8+CLIP实现图文特征匹配

本文通过结合YOLOv8s的高效物体检测能力与CLIP的先进图像-文本匹配技术&#xff0c;展示了深度学习在处理和分析复杂多模态数据中的潜力。这种技术的应用不仅限于学术研究&#xff0c;还能广泛应用于工业、商业和日常技术产品中&#xff0c;以实现更智能的人机交互和信息处理。…

第四届微调——炼丹

学习地址&#xff1a;Tutorial/xtuner/README.md at main InternLM/Tutorial GitHub 笔记 微调是一种在已有的预训练模型基础上&#xff0c;通过使用新的数据对模型进行进一步优化和调整的技术手段。它的目的是使模型能够更好地适应特定的应用场景和任务需求&#xff0c;进一…

IDEA切换分支

方法一 1、选择要切换分支的module 2、右键&#xff0c;选择git 3、再点击branches 4、可以看到当前module的本地分支&#xff08;local Branches&#xff09;及远程分支&#xff08;Remote Branches&#xff09;列表。点击你要切换到的分支,Checkout即可。 方法二 1、点击…