Java对象转换方案分析与mapstruct实践

简介: 随着系统模块分层不断细化,在Java日常开发中不可避免地涉及到各种对象的转换,如:DO、DTO、VO等等,编写映射转换代码是一个繁琐重复且还易错的工作,一个好的工具辅助,减轻了工作量、提升开发工作效率的同时还能减少bug的发生

image.png

作者 | 久贤
来源 | 阿里技术公众号

一 前言

随着系统模块分层不断细化,在Java日常开发中不可避免地涉及到各种对象的转换,如:DO、DTO、VO等等,编写映射转换代码是一个繁琐重复且还易错的工作,一个好的工具辅助,减轻了工作量、提升开发工作效率的同时还能减少bug的发生。

二 常用方案及分析

1 fastjson

CarDTO entity = JSON.parseObject(JSON.toJSONString(carDO), CarDTO.class);

这种方案因为通过生成中间json格式字符串,然后再转化成目标对象,性能非常差,同时因为中间会生成json格式字符串,如果转化过多,gc会非常频繁,同时针对复杂场景支持能力不足,基本很少用。

2 BeanUtil类

BeanUtil.copyProperties()结合手写get、set,对于简单的转换直接使用BeanUtil,复杂的转换自己手工写get、set。该方案的痛点就在于代码编写效率低、冗余繁杂还略显丑陋,并且BeanUtil因为使用了反射invoke去赋值性能不高。

只能适合bean数量较少、内容不多、转换不频繁的场景。

apache.BeanUtils

org.apache.commons.beanutils.BeanUtils.copyProperties(do, entity);

这种方案因为用到反射的原因,同时本身设计问题,性能比较差。集团开发规约明确规定禁止使用。

image.png

spring.BeanUtils

org.springframework.beans.BeanUtils.copyProperties(do, entity);

这种方案针对apache的BeanUtils做了很多优化,整体性能提升不少,不过还是使用反射实现比不上原生代码处理,其次针对复杂场景支持能力不足。

image.png

3 beanCopier

BeanCopier copier = BeanCopier.create(CarDO.class, CarDTO.class, false); 
copier.copy(do, dto, null);

这种方案动态生成一个要代理类的子类,其实就是通过字节码方式转换成性能最好的get和set方式,重要的开销在创建BeanCopier,整体性能接近原生代码处理,比BeanUtils要好很多,尤其在数据量很大时,但是针对复杂场景支持能力不足。

4 各种Mapping框架

分类

Object Mapping 技术从大的角度来说分为两类,一类是运行期转换,另一类则是编译期转换:

  • 运行期反射调用 set/get 或者是直接对成员变量赋值。这种方式通过invoke执行赋值,实现时一般会采用beanutil, Javassist等开源库。运行期对象转换的代表主要是Dozer和ModelMaper。
  • 编译期动态生成 set/get 代码的class文件,在运行时直接调用该class的 set/get 方法。该方式实际上仍会存在 set/get 代码,只是不需要开发人员自己写了。这类的代表是:MapStruct,Selma,Orika。

分析

  • 无论哪种Mapping框架,基本都是采用xml配置文件 or 注解的方式供用户配置,然后生成映射关系。
  • 编译期生成class文件方式需要DTO仍然有set/get方法,只是调用被屏蔽;而运行期反射方式在某些直接填充 field的方案中,set/get代码也可以省略。
  • 编译期生成class方式会有源代码在本地,方便排查问题。
  • 编译期生成class方式因为在编译期才出现java和class文件,所以热部署会受到一定影响。
  • 反射型由于很多内容是黑盒,在排查问题时,不如编译期生成class方式方便。参考GitHub上工程java-object-mapper-benchmark可以看出主要框架性能比较。
  • 反射型调用由于是在运行期根据映射关系反射执行,其执行速度会明显下降N个量级。
  • 通过编译期生成class代码的方式,本质跟直接写代码区别不大,但由于代码都是靠模板生成,所以代码质量没有手工写那么高,这也会造成一定的性能损失。

image.png

综合性能、成熟度、易用性、扩展性,mapstruct是比较优秀的一个框架。

三 Mapstruct使用指南

1 Maven引入

f3ec68fd4f6c796b0c2e0ef5e8c600b.jpg

2 简单入门案例

DO和DTO

这里用到了lombok简化代码,lombok的原理也是在编译时去生成get、set等被简化的代码。

@Data 
public class Car {     private String make;     private int numberOfSeats;     private CarType type; 
}
@Data 
public class CarDTO {     private String make;     private int seatCount;     private String type; 
}

定义Mapper

@Mapper中描述映射,在编辑的时候mapstruct将会根据此描述生成实现类:

  • 当属性与其目标实体副本同名时,它将被隐式映射。
  • 当目标实体中的属性具有不同名称时,可以通过@Mapping注释指定其名称。
@Mapper 
public interface CarMapper {     @Mapping(source = "numberOfSeats", target = "seatCount")     CarDTO CarToCarDTO(Car car); }

使用Mapper

通过Mappers 工厂生成静态实例使用。

@Mapper 
public interface CarMapper {     CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);  @Mapping(source = "numberOfSeats", target = "seatCount")     CarDTO CarToCarDTO(Car car); 
}
Car car = new Car(...); 
CarDTO carDTO = CarMapper.INSTANCE.CarToCarDTO(car);

getMapper会去load接口的Impl后缀的实现类。

image.png

通过生成spring bean注入使用,Mapper注解加上spring配置,会自动生成一个bean,直接使用bean注入即可访问。

@Mapper(componentModel = "spring") 
public interface CarMapper {     @Mapping(source = "numberOfSeats", target = "seatCount")     CarDTO CarToCarDTO(Car car); 
}

自动生成的MapperImpl内容

如果配置了spring bean访问会在注解上自动加上@Component。

image.png

3 进阶使用

逆向映射

如果是双向映射,例如 从DO到DTO以及从DTO到DO,正向方法和反向方法的映射规则通常是相似的,并且可以通过切换源和目标来简单地逆转。

使用注解@InheritInverseConfiguration 指示方法应继承相应反向方法的反向配置。

@Mapper 
public interface CarMapper {     CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);    @Mapping(source = "numberOfSeats", target = "seatCount")     CarDTO CarToCarDTO(Car car);    @InheritInverseConfiguration     Car CarDTOToCar(CarDTO carDTO); 
}

更新bean映射

有些情况下不需要映射转换产生新的bean,而是更新已有的bean。

@Mapper 
public interface CarMapper {     CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     @Mapping(source = "numberOfSeats", target = "seatCount")     void updateDTOFromCar(Car car, @MappingTarget CarDTO carDTO);

集合映射

集合类型(List,Set,Map等)的映射以与映射bean类型相同的方式完成,即通过在映射器接口中定义具有所需源类型和目标类型的映射方法。MapStruct支持Java Collection Framework中的多种可迭代类型。

生成的代码将包含一个循环,该循环遍历源集合,转换每个元素并将其放入目标集合。如果在给定的映射器或其使用的映射器中找到用于集合元素类型的映射方法,则将调用此方法以执行元素转换,如果存在针对源元素类型和目标元素类型的隐式转换,则将调用此转换。

@Mapper 
public interface CarMapper {     CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "numberOfSeats", target = "seatCount")     CarDTO CarToCarDTO(Car car);     List<CarDTO> carsToCarDtos(List<Car> cars);     Set<String> integerSetToStringSet(Set<Integer> integers);     @MapMapping(valueDateFormat = "dd.MM.yyyy")     Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source); 
}

编译时生成的实现类:

image.png

多个源参数映射

MapStruct 还支持具有多个源参数的映射方法。例如,将多个实体组合成一个数据传输对象。

在原案例新增一个Person对象,CarDTO中新增driverName属性,根据Person对象获得。

@Mapper 
public interface CarMapper {     CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     @Mapping(source = "car.numberOfSeats", target = "seatCount")     @Mapping(source = "person.name", target = "driverName")     CarDTO CarToCarDTO(Car car, Person person); }

编译生成的代码:

image.png

默认值和常量映射

如果相应的源属性是null ,则可以指定默认值以将预定义值设置为目标属性。在任何情况下,都可以指定常量来设置这样的预定义值。默认值和常量被指定为字符串值。当目标类型是原始类型或装箱类型时,String 值将采用字面量,在这种情况下允许位/八进制/十进制/十六进制模式,只要它们是有效的文字即可。在所有其他情况下,常量或默认值会通过内置转换或调用其他映射方法进行类型转换,以匹配目标属性所需的类型。

@Mapper 
public interface SourceTargetMapper {     SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );     @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")     @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")     @Mapping(target = "stringConstant", constant = "Constant Value")     @Mapping(target = "integerConstant", constant = "14")     @Mapping(target = "longWrapperConstant", constant = "3001")     @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")     @Mapping(target = "stringListConstants", constant = "jack-jill-tom")     Target sourceToTarget(Source s); 
}

自定义映射方法或映射器

在某些情况下,可能需要手动实现 MapStruct 无法生成的从一种类型到另一种类型的特定映射。

可以在Mapper中定义默认实现方法,生成转换代码将调用相关方法:

@Mapper 
public interface CarMapper {     CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     @Mapping(source = "numberOfSeats", target = "seatCount")     @Mapping(source = "length", target = "lengthType")     CarDTO CarToCarDTO(Car car);     default String getLengthType(int length) {         if (length > 5) {             return "large";         } else {             return "small";         }     } 
}

也可以定义其他映射器,如下案例Car中Date需要转换成DTO中的String:

public class DateMapper {     public String asString(Date date) {         return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).format( date ) : null;     }     public Date asDate(String date) {         try {             return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).parse( date ) : null;         } catch ( ParseException e ) {             throw new RuntimeException( e );         }     } 
}
@Mapper(uses = DateMapper.class) 
public interface CarMapper {     CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     @Mapping(source = "numberOfSeats", target = "seatCount")     CarDTO CarToCarDTO(Car car); 
}

编译生成的代码:

image.png

若遇到多个类似的方法调用时会出现模棱两可,需使用@qualifiedBy指定:

@Mapper 
public interface CarMapper {     CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     @Mapping(source = "numberOfSeats", target = "seatCount")     @Mapping(source = "length", target = "lengthType", qualifiedByName = "newStandard")     CarDTO CarToCarDTO(Car car);     @Named("oldStandard")     default String getLengthType(int length) {         if (length > 5) {             return "large";         } else {             return "small";         }     }     @Named("newStandard")     default String getLengthType2(int length) {         if (length > 7) {             return "large";         } else {             return "small";         }     } 
}

表达式自定义映射

通过表达式,可以包含来自多种语言的结构。

目前仅支持 Java 作为语言。例如,此功能可用于调用构造函数,整个源对象都可以在表达式中使用。应注意仅插入有效的 Java 代码:MapStruct 不会在生成时验证表达式,但在编译期间生成的类中会显示错误。

@Data 
@AllArgsConstructor 
public class Driver {     private String name;     private int age; 
}
@Mapper 
public interface CarMapper {     CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     @Mapping(source = "car.numberOfSeats", target = "seatCount")     @Mapping(target = "driver", expression = "java( new com.alibaba.my.mapstruct.example4.beans.Driver(person.getName(), person.getAge()))")     CarDTO CarToCarDTO(Car car, Person person); 
} 

默认表达式是默认值和表达式的组合:

@Mapper( imports = UUID.class )
public interface SourceTargetMapper {     SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );     @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")     Target sourceToTarget(Source s); 
}

装饰器自定义映射

在某些情况下,可能需要自定义生成的映射方法,例如在目标对象中设置无法由生成的方法实现设置的附加属性。

实现起来也很简单,用装饰器模式实现映射器的一个抽象类,在映射器Mapper中添加注解@DecoratedWith指向装饰器类,使用时还是正常调用。

@Mapper 
@DecoratedWith(CarMapperDecorator.class) 
public interface CarMapper {     CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     @Mapping(source = "numberOfSeats", target = "seatCount")     CarDTO CarToCarDTO(Car car); 
}
public abstract class CarMapperDecorator implements CarMapper {     private final CarMapper delegate;     protected CarMapperDecorator(CarMapper delegate) {         this.delegate = delegate;     }     @Override     public CarDTO CarToCarDTO(Car car) {         CarDTO dto = delegate.CarToCarDTO(car);         dto.setMakeInfo(car.getMake() + " " + new SimpleDateFormat( "yyyy-MM-dd" ).format(car.getCreateDate()));         return dto;     } 
}

原文链接
本文为阿里云原创内容,未经允许不得转载。 

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

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

相关文章

致敬 hacker |盘点内存虚拟化探索之路

简介&#xff1a; 内存虚拟化相比裸机&#xff0c;仍然存在较大差异&#xff0c;是当下值得关注的问题&#xff01; 云与虚拟化 云计算是通过 Internet 服务的方式提供动态可伸缩资源的计算模式&#xff0c;经过多年的发展已成为企业 IT 技术的重要支撑。虚拟化是云计算的核心…

minwindow java_java中setMinWindowLayout()是什么呀?

展开全部分析代码 ,可以看出SInfo 是JFrame的子类.setMinWindowLayout() 方法,就可能就是该类自定义的方法,因为JFrame并没e68a8462616964757a686964616f31333431373937有该方法.根据该方法名推测, 这个方法应该就是设置界面布局的方法,主要用于调整组件(按钮,输入框...等)的位…

走进新华三解决方案 360°业务能力中心,读懂新华三如何助力企业数字化转型

随着数字化转型步入“深水区”&#xff0c;云计算、5G、AI 等新一代技术落地应用各行各业。企业对“端到端”解决方案的需求愈发强烈&#xff0c;亟待统一规划、建设和交付的“端到端”解决方案。对此&#xff0c;作为业界领先的数字化解决方案领导者&#xff0c;新华三自 2019…

阿里云马涛:什么是操作系统的云原生?

简介&#xff1a; 云原生已经成为IT界最流行的一个定语&#xff0c;似乎不谈云原生就out了&#xff0c;但什么才是真正的云原生&#xff1f; 注&#xff1a;本文作者马涛&#xff0c;阿里云智能研究员、阿里巴巴集团内核团队创始人之一、阿里云基础软件部操作系统团队负责人。…

pip 安装_安装 pip 轻松管理 PyPI 软件包 | Linux 中国

在 Linux、Mac 或 Windows 上为旧版 Python 安装 pip。-- Vijay Singh KhatriPython 是一种功能强大、流行广泛的编程语言&#xff0c;在常规编程、数据科学等很多方面它都有丰富的软件包可供使用。但这些软件包通常都不会在 Python 安装时自动附带&#xff0c;而是需要由用户自…

技术解析:一文看懂 Anolis OS 国密生态 | 龙蜥专场

简介&#xff1a; Anolis OS国密是社区在Anolis OS上做的国密技术解决方案。 编者注&#xff1a;本文系两位演讲者整理&#xff0c;他们在2021年阿里云开发者大会的「开源操作系统社区和生态分论坛」上带了分享&#xff0c;演讲主题为《国密技术开发与实践》&#xff0c;为国内…

场景联创 施耐德电气“绿色智能制造创赢计划”第二季收官

中国上海&#xff0c;2021年12月21日——今日&#xff0c;由工业和信息化部国际经济技术合作中心&#xff08;工信部国合中心&#xff09;与施耐德电气主办的“绿色智能制造创赢计划”第二季总决赛在上海举办。经过半年多的加速营培训、场景探访与联合方案开发&#xff0c;最终…

DLF +DDI 一站式数据湖构建与分析最佳实践

简介&#xff1a; 本文由阿里云数据湖构建 DLF 团队和 Databricks 数据洞察团队联合撰写&#xff0c;旨在帮助您更深入地了解阿里云数据湖构建&#xff08;DLF&#xff09;Databricks 数据洞察&#xff08;DDI&#xff09;构建一站式云上数据入湖。 作者陈鑫伟&#xff08;熙康…

pip升级python包命令_python安装扩展库常用的是什么工具

pip 是 Python 包管理工具&#xff0c;该工具提供了对Python 包的查找、下载、安装、卸载的功能。目前如果你在 http://python.org 下载最新版本的安装包&#xff0c;则是已经自带了该工具。Python 2.7.9 或 Python 3.4 以上版本都自带 pip 工具。pip 官网&#xff1a;https:/…

获国际架构顶会ATC2021最佳论文!Fuxi2.0去中心化的调度架构详解

简介&#xff1a; 近日&#xff0c;在国际体系架构顶会USENIX ATC2021上&#xff0c;阿里云飞天伏羲团队与香港中文大学合作的一篇论文《Scaling Large Production Clusters with Partitioned Synchronization》不仅成功被大会录取&#xff0c;而且被大会专家组评定为三篇最佳论…

svg入门经典pdf_机器学习最好的入门课程是什么?

最近老胡不太忙了&#xff0c;项目告一段落&#xff0c;摸鱼时间多了一些。昨天我翻译了一个思维导图《超详细的人工智能专家路线图》强烈建议没有看的同学看一看&#xff0c;查漏补缺必备机器学习从 入 门 到 精 通 路 线 图翻完之后自己也有很大的收获&#xff0c;所以准备干…

MaxCompute跨境访问加速解决方案

简介&#xff1a; MaxCompute联合全球加速服务&#xff0c;为有跨境访问需求的MaxCompute客户提供一套高效稳定的跨境访问加速方案。 MaxCompute联合全球加速服务&#xff0c;为有跨境访问需求的MaxCompute客户提供一套高效稳定的跨境访问加速方案。 背景信息 MaxCompute的大…

借助钉钉宜搭,奶茶店开始用黑科技管理门店了

简介&#xff1a; 投诉处理的速度变快了——这是7分甜消费者在这个初夏最直观的感受&#xff0c;借助钉钉宜搭的低代码能力实现的数字化管理&#xff0c;过去7分甜的许多管理痛点&#xff0c;现在正被一一攻克。 一方小小的奶茶零售门店&#xff0c;内里乾坤万千。7分甜成立于2…

爬虫爬到的网页源代码不是真正的源代码_Python爬虫经常爬不到数据?你可以看一下这篇文章...

近期&#xff0c;通过做了一些小的项目&#xff0c;觉得对于Python爬虫有了一定的了解&#xff0c;于是&#xff0c;就对于Python爬虫爬取数据做了一个小小的总结&#xff0c;希望大家喜欢&#xff01;1.最简单的Python爬虫最简单的Python爬虫莫过于直接使用urllib.request.url…

德勤发布2021年度企业AI应用现状报告,AI驱动已成竞争必需品

1、报告显示&#xff0c;与全球企业相比&#xff0c;中国企业对AI的负面影响&#xff0c;表现出了更多恐惧和担忧。 2、战略上&#xff0c;报告鼓励企业抓住机会&#xff0c;相信一切皆有可能&#xff0c;不要让“成本支出”的目标导致错失良机。 3、报告显示尽管真正意义上的“…

试着读懂你的心——闲鱼聊天小助手的探索之路

简介&#xff1a; 读懂你的心 作者&#xff1a;闲鱼技术——有攸 一、背景&#xff1a; 卡耐基在《人性的弱点》一书中说&#xff1a;“世界上唯一能影响他人的方法&#xff0c;就是谈论他所要的&#xff0c;而且还要告诉他&#xff0c;如何才能得到他所要的”。由此可见&am…

python 闭包_Python中的闭包

一、什么是闭包在谈之前&#xff0c;我们先来说说作用域&#xff0c;变量的作用域无非就两种&#xff1a;全局变量和局部变量。函数内部可以直接读取全局变量&#xff0c;但是在函数外部无法读取函数内部的局部变量。出于种种原因&#xff0c;我们有时候需要获取到函数内部的局…

Flink 在爱奇艺广告业务的实践

简介&#xff1a; 5 月 22 日北京站 Flink Meetup 分享的议题。 本文整理自爱奇艺技术经理韩红根在 5 月 22 日北京站 Flink Meetup 分享的议题《Flink 在爱奇艺广告业务的实践》&#xff0c;内容包括&#xff1a; 业务场景业务实践Flink 使用过程中的问题及解决未来规划一、业…

使用 Flomesh 强化 Spring Cloud 服务治理

作者 | Addo Zhang来源 | 云原生指北写在最前这篇是关于如何使用 Flomesh[1] 服务网格来强化 Spring Cloud 的服务治理能力&#xff0c;降低 Spring Cloud 微服务架构落地服务网格的门槛&#xff0c;实现“自主可控”。架构Architect环境搭建搭建 Kubernetes 环境&#xff0c;可…

如何避免JS内存泄漏?

简介&#xff1a; 很多开发者可能平时并不关心自己维护的页面是否存在内存泄漏&#xff0c;原因可能是刚开始简单的页面内存泄漏的速度很缓慢&#xff0c;在造成严重卡顿之前可能就被用户刷新了&#xff0c;问题也就被隐藏了&#xff0c;但是随着页面越来越复杂&#xff0c;尤其…