Java对象转换方案分析与mapstruct实践
简介: 随着系统模块分层不断细化,在Java日常开发中不可避免地涉及到各种对象的转换,如:DO、DTO、VO等等,编写映射转换代码是一个繁琐重复且还易错的工作,一个好的工具辅助,减轻了工作量、提升开发工作效率的同时还能减少bug的发生
作者 | 久贤
来源 | 阿里技术公众号
一 前言
随着系统模块分层不断细化,在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);
这种方案因为用到反射的原因,同时本身设计问题,性能比较差。集团开发规约明确规定禁止使用。
spring.BeanUtils
org.springframework.beans.BeanUtils.copyProperties(do, entity);
这种方案针对apache的BeanUtils做了很多优化,整体性能提升不少,不过还是使用反射实现比不上原生代码处理,其次针对复杂场景支持能力不足。
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代码的方式,本质跟直接写代码区别不大,但由于代码都是靠模板生成,所以代码质量没有手工写那么高,这也会造成一定的性能损失。
综合性能、成熟度、易用性、扩展性,mapstruct是比较优秀的一个框架。
三 Mapstruct使用指南
1 Maven引入
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后缀的实现类。
通过生成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。
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);
}
编译时生成的实现类:
多个源参数映射
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); }
编译生成的代码:
默认值和常量映射
如果相应的源属性是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);
}
编译生成的代码:
若遇到多个类似的方法调用时会出现模棱两可,需使用@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;
}
}
原文链接
本文为阿里云原创内容,未经允许不得转载。
Java对象转换方案分析与mapstruct实践的更多相关文章
- Java对象转换成xml对象和Java对象转换成JSON对象
1.把Java对象转换成JSON对象 apache提供的json-lib小工具,它可以方便的使用Java语言来创建JSON字符串.也可以把JavaBean转换成JSON字符串. json-lib的核心 ...
- JAVA对象转换JSON
1. 把java 对象列表转换为json对象数组,并转为字符串 复制代码 代码如下: JSONArray array = JSONArray.fromObject(userlist); String ...
- mapstruct解放Java对象转换
摘要 当前web后端开发,都是使用多层工程结构,需要在VO,BO,DTO,DO等各种数据结构中相互转换.这些转换代码都是些比较简单的字段映射,类型转换,重复性工作比较高,可以使用一些工具解放我们的双手 ...
- Java对象拷贝原理剖析及最佳实践
作者:宁海翔 1 前言 对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po.Dto.Do.Vo各个表现层数据的转换,也存在于系统交互如序列化.反序列化. Java对象拷贝分为深拷贝和浅拷贝,目前 ...
- java对象转换
对象转换: 对象的分层涉及到各个层级之间的对象转换(Entity2DTO , DTO2VO, VO2DTO,DTO2Entity等),传统的采用set/get 方法硬编码实现写的代码比较多:或者采用B ...
- FastJson、Jackson、Gson进行Java对象转换Json细节处理
前言 Java对象在转json的时候,如果对象里面有属性值为null的话,那么在json序列化的时候要不要序列出来呢?对比以下json转换方式 一.fastJson 1.fastJson在转换java ...
- FastJson、Jackson、Gson进行Java对象转换Json的细节处理
前言 Java对象在转json的时候,如果对象里面有属性值为null的话,那么在json序列化的时候要不要序列出来呢?对比以下json转换方式 一.fastJson 1.fastJson在转换java ...
- 总结学习! xml与java对象转换 --- JDK自带的JAXB(Java Architecture for XML Binding)
JAXB(Java Architecture for XML Binding) 是一个业界的标准,是一项可以根据XML Schema产生Java类的技术.该过程中,JAXB也提供了将XML实例文档反向 ...
- jackson 进行json与java对象转换 之三
2.测试类,没用Junit,用Main()方法输出. package test; import java.io.IOException; import java.util.ArrayList; imp ...
- jxcel - 好用的Excel与Java对象转换工具
更多精彩博文,欢迎访问我的个人博客 Jxcel简介 Jxcel是一个支持Java对象与Excel(目前仅xlsx.xls)互相转换的工具包. 项目地址:https://github.com/jptan ...
随机推荐
- 24_用Qt和FFmpeg实现简单的YUV播放器
前面文章FFmpeg像素格式转换中我们使用FFmpeg实现了一个像素格式转换工具类,现在我们就可以在Qt中利用QImage很容易的实现一个简单的YUV播放器了. 播放器功能很简单,只有播放.暂停和停止 ...
- 记录-js基础练习题
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 隔行换色(%): window.onload = function() { var aLi = document.getElementsB ...
- C# Image 图片缩放 截取
从大图中截取一部分图片 /// <summary> /// 从大图中截取一部分图片 /// </summary> /// <param name="fromIm ...
- C# 强大的网页处理类NSoup
地址: https://github.com/GeReV/NSoup using System; using System.Collections.Generic; using System.Comp ...
- SpringCloud微服务集成Dubbo
1.Dubbo介绍 Apache Dubbo 是一款易用.高性能的 WEB 和 RPC 框架,同时为构建企业级微服务提供服务发现.流量治理.可观测.认证鉴权等能力.工具与最佳实践.用于解决微服务架构下 ...
- GFLV2:边界框不确定性的进一步融合,提点神器 | CVPR 2021
GFLV2基于GFLV1的bbox分布进行改进,将分布的统计信息融入到定位质量估计中,整体思想十分创新和完备,从实验结果来看,效果还是挺不错的 来源:晓飞的算法工程笔记 公众号 论文: Gener ...
- C++原子操作与内存序 1
问题 #include<iostream> #include<thread> int main() { int sum = 0; auto f = [&sum]() { ...
- sklearn数据集使用(鸢尾花)
1 2 from sklearn.datasets import load_iris 3 4 """ 5 sklearn数据集使用 6 :return: 7 " ...
- IPv4地址的结构体与网络字节序
IPv4地址的结构体 /* Fixed-size types, underlying types depend on word size and compiler. */ typedef signed ...
- 《2020年IT行业项目管理调查报告》重磅发布
近年来,IT行业迅速发展,物联网.敏捷.DevOps等已成为行业的热门话题.为更好地了解行业现状,禅道项目管理软件联合各合作伙伴于2021年1月开展了针对IT行业的问卷调查,并推出了<2020年 ...