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

news2024/11/24 11:02:58

文章目录

    • 工程师工具箱系列(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包]

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024年深圳市教师招聘报名流程(建议电脑)

2024年深圳市教师招聘报名流程&#xff08;建议电脑&#xff09; #深圳教师招聘 #深圳教招 #深圳教师招聘考试 #教师招聘报名照片处理 #深圳教师招聘笔试

法语语式与时态总结,柯桥零基础学法语

常用语式 法语中的常用语式分为&#xff1a;直陈式、条件式、虚拟式、命令式、不定式与分词式。 直陈式&#xff08;lindicatif&#xff09;初学法语时首先就要学直陈式&#xff0c;也是最常用的语式&#xff0c;表示确实发生的动作。 条件式&#xff08;le conditionnel&am…

动态规划----股票买卖问题(详解)

目录 一.买卖股票的最佳时机&#xff1a; 二.买卖股票的最佳时机含冷冻期&#xff1a; 三.买卖股票的最佳时期含⼿续费&#xff1a; 四.买卖股票的最佳时机III: 五.买卖股票的最佳时机IV: 买卖股票的最佳时机问题介绍&#xff1a;动态规划买卖股票的最佳时机是一个经典的…

LeetCode 题目 119:杨辉三角 II

作者介绍&#xff1a;10年大厂数据\经营分析经验&#xff0c;现任字节跳动数据部门负责人。 会一些的技术&#xff1a;数据分析、算法、SQL、大数据相关、python&#xff0c;欢迎探讨交流 欢迎加入社区&#xff1a;码上找工作 作者专栏每日更新&#xff1a; LeetCode解锁1000题…

Windows11“重置此电脑”后,Edge浏览器在微软应用商店显示“已安装”,但是开始菜单搜索不到的解决办法

Windows11“重置此电脑”后&#xff0c;Edge浏览器在微软应用商店显示“已安装”&#xff0c;但是开始菜单搜索不到的解决办法 为什么重新使用Edge&#xff1f;问题描述不该更新可用更新问过AI&#xff08;通义千问&#xff09;&#xff0c;并且AI提供方法全都无效。现象 操作步…

python3如何安装bs4

在python官网找到beautifulsoup模块的下载页面&#xff0c;点击"downloap"将该模块的安装包下载到本地。 将该安装包解压&#xff0c;然后在打开cmd&#xff0c;并通过cmd进入到该安装包解压后的文件夹目录下。 在该文件目录下输入"python install setup.py&quo…

nss刷题(2)

1、[NSSCTF 2022 Spring Recruit]ezgame 打开题目是一个游戏界面 发现是有分数的&#xff0c;猜测分数达到某个之后可以获得flag&#xff0c;查看源码看一下 看到末尾显示分数超过65后显示flag 在js中找到了一个score,将他的值改为大于65的数后随意玩一次就可以得到flag同时&a…

Python使用Rembg库去除图片背景

一、引入Rembg库 #库地址 https://github.com/danielgatis/rembg#CPU使用 pip install rembg # for library pip install rembg[cli] # for library cli#GPU使用&#xff08;系统支持onnxruntime-gpu&#xff09; pip install rembg[gpu] # for library pip install rembg[gp…

JAVA 集合(单列集合)

集合框架 1.集合的特点 a.只能存储引用数据类型的数据 b.长度可变 c.集合中有大量的方法,方便我们操作 2.分类: a.单列集合:一个元素就一个组成部分: list.add(“张三”) b.双列集合:一个元素有两部分构成: key 和 value map.put(“涛哥”,“金莲”) -> key,value叫做键值…

常用Linux命令详细总结

一、文档编辑、过滤、查看命令 1、cp 复制文件和目录 -a 复制文件并保持文件属性 -d 若源文件为链接文件&#xff0c;则复制链接文件属性而非文件本身 -i 覆盖文件前提示&#xff0c;如果不要提示&#xff0c;在命令前加上\ -r 递归复制&#xff0c;通常用于目录的复制 …

[muduo网络库]——muduo库Buffer类(剖析muduo网络库核心部分、设计思想)

接着之前我们[muduo网络库]——muduo库Socket类&#xff08;剖析muduo网络库核心部分、设计思想&#xff09;&#xff0c;我们接下来继续看muduo库中的Buffer类。其实Buffer在我的另一篇博客里面已经介绍过了深究muduo网络库的Buffer类&#xff01;&#xff01;&#xff01;&am…

QTreeView学习 branch 虚线设置

1、方法一&#xff1a; #include <QStyleFactory> ui.treeView->setStyle(QStyleFactory::create("windows")); 2、方法二&#xff1a; QString strtyle2 R"( QTreeView::branch:has-siblings:!adjoins-item { border-image: url(:/TreeViewDe…

docker+nginx+Jenkins自动构建

文章目录 前言一、实操记录问下AI&#xff1a;jenkins 配置新增一个mobilegit配置Build TriggersBuild EnvironmentBuild StepsPost-build Actions 上面一顿配置下来&#xff0c;构建 -- FAILURE 总结 前言 在已有docker-Jenkins-nginx 部署方案上&#xff0c;在另外一台测试…

KAN神经网络简短介绍

KANs简介 Kolmogorov-Arnold Networks (KANs) 是一种创新的神经网络模型&#xff0c;它挑战了传统多层感知器(MLPs)的设计&#xff0c;通过将激活函数从节点转移到边上来提升模型的性能和可解释性。KAN的核心在于&#xff0c;其所有权重参数均被单变量的样条函数代替&#xff…

C++(week3):C语言文件操作

文章目录 (十二) 文件1.流(1)流模型(2)程序员视角的文件(3)缓冲区类型(4)标准流(5)二进制文件 与 文本文件(6)文件流的接口(API) 2.打开/关闭文件(1)fopen(2)fclose(3)示例代码 3.读/写文件(1)fgetc / fputc&#xff1a;一个字符一个字符地读写(2)fgets / fputs&#xff1a;一行…

pytest + yaml 框架 - 录制接口转 yaml 用例实现

pytest yaml 框架基本不用写 python 代码&#xff0c;只需写yaml 文件用例就能实现接口自动化。 现在引入接口录制功能&#xff0c;连 yaml 文件也不用写了&#xff0c;点点点就能生成 yaml 用例文件了。 录制功能在v1.3.4版本上实现 pip instal pytest-yaml-yoyo 环境准备 …

string类的介绍与使用【C++】

string类 前言一、为什么学习string类C语言中的字符串示例 二、标准库中的string类string类string类的常用接口说明string类对象的常见构造string类对象的容量操作string的接口测试及使用string类对象的访问及遍历操作下标和方括号遍历范围for遍历迭代器遍历相同的代码&#xf…

Seaborn : 超好用的Python可视化工具

1. 引言 说到数据可视化&#xff0c;Seaborn就像一颗隐藏的宝石&#xff01;在进行探索性数据分析时&#xff0c;我们通常从Matplotlib 开始&#xff0c;而对 Seaborn 的探索相对较少&#xff01;但是&#xff0c;只要你了解 Seaborn 的全部潜力&#xff0c;你就会惊奇地发现&…

半小时搞懂STM32面经知识点——IIC

1.IIC 1.1什么是IIC&#xff1f; 同步半双工通信协议&#xff0c;适用于小数据和短距离传输。 1.2 IIC需要几条线&#xff1f; IIC总共有2条通信总线&#xff08;SDA,SCL&#xff09;&#xff0c;SCL为时钟同步线&#xff0c;用于主机和从机间数据同步操作&#xff1b;SDA为…

Qt开发常见报错大全与解决办法

下面的报错是我日常开发经常遇到的,对着下面的解决方法一招搞定就行了。 我们没必要都去记住,只需要见方抓药即可。 目前版本有27个常见报错,持续更新中。 常见报错 翻译不起作用 你可能改了类名字,但是.ts文件里没有跟着改。 Cannot send events to objects owned by a…