SpringBoot 如何进行对象复制,老鸟们都这么玩的!
大家好,我是飘渺。
今天带来SpringBoot老鸟系列的第四篇,来聊聊在日常开发中如何优雅的实现对象复制。
首先我们看看为什么需要对象复制?
为什么需要对象复制

如上,是我们平时开发中最常见的三层MVC架构模型,编辑操作时Controller层接收到前端传来的DTO对象,在Service层需要将DTO转换成DO,然后在数据库中保存。查询操作时Service层查询到DO对象后需要将DO对象转换成VO对象,然后通过Controller层返回给前端进行渲染。
这中间会涉及到大量的对象转换,很明显我们不能直接使用getter/setter复制对象属性,这看上去太low了。想象一下你业务逻辑中充斥着大量的getter&setter,代码评审时老鸟们会如何笑话你?

所以我们必须要找一个第三方工具来帮我们实现对象转换。
看到这里有同学可能会问,为什么不能前后端都统一使用DO对象呢?这样就不存在对象转换呀?
设想一下如果我们不想定义 DTO 和 VO,直接将 DO 用到数据访问层、服务层、控制层和外部访问接口上。此时该表删除或则修改一个字段,DO 必须同步修改,这种修改将会影响到各层,这并不符合高内聚低耦合的原则。通过定义不同的 DTO 可以控制对不同系统暴露不同的属性,通过属性映射还可以实现具体的字段名称的隐藏。不同业务使用不同的模型,当一个业务发生变更需要修改字段时,不需要考虑对其它业务的影响,如果使用同一个对象则可能因为 “不敢乱改” 而产生很多不优雅的兼容性行为。
对象复制工具类推荐
对象复制的类库工具有很多,除了常见的Apache的BeanUtils,Spring的BeanUtils,Cglib BeanCopier,还有重量级组件MapStruct,Orika,Dozer,ModelMapper等。
如果没有特殊要求,这些工具类都可以直接使用,除了Apache的BeanUtils。原因在于Apache BeanUtils底层源码为了追求完美,加了过多的包装,使用了很多反射,做了很多校验,所以导致性能较差,并在阿里巴巴开发手册上强制规定避免使用 Apache BeanUtils。

至于剩下的重量级组件,综合考虑其性能还有使用的易用性,我这里更推荐使用Orika。Orika底层采用了javassist类库生成Bean映射的字节码,之后直接加载执行生成的字节码文件,在速度上比使用反射进行赋值会快很多。
国外大神 baeldung 已经对常见的组件性能进行过详细测试,大家可以通过 https://www.baeldung.com/java-performance-mapping-frameworks 查看。
Orika基本使用
要使用Orika很简单,只需要简单四步:
- 引入依赖
<dependency>
<groupid>ma.glasnost.orika</groupid>
<artifactid>orika-core</artifactid>
<version>1.5.4</version>
</dependency>
- 构造一个MapperFactory
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
- 注册字段映射
mapperFactory.classMap(SourceClass.class, TargetClass.class)
.field("firstName", "givenName")
.field("lastName", "sirName")
.byDefault()
.register();
当字段名在两个实体不一致时可以通过.field()方法进行映射,如果字段名都一样则可省略,byDefault()方法用于注册名称相同的属性,如果不希望某个字段参与映射,可以使用exclude方法。
- 进行映射
MapperFacade mapper = mapperFactory.getMapperFacade();
SourceClass source = new SourceClass();
// set some field values
...
// map the fields of 'source' onto a new instance of PersonDest
TargetClass target = mapper.map(source, TargetClass.class);
经过上面四步我们就完成了SourceClass到TargetClass的转换。至于Orika的其他使用方法大家可以参考 http://orika-mapper.github.io/orika-docs/index.html
看到这里,肯定有粉丝会说:你这推荐的啥玩意呀,这个Orika使用也不简单呀,每次都要这先创建MapperFactory,建立字段映射关系,才能进行映射转换。
别急,我这里给你准备了一个工具类OrikaUtils,你可以通过文末github仓库获取。
它提供了五个公共方法:

分别对应:
- 字段一致实体转换
- 字段不一致实体转换(需要字段映射)
- 字段一致集合转换
- 字段不一致集合转换(需要字段映射)
- 字段属性转换注册
接下来我们通过单元测试案例重点介绍此工具类的使用。
Orika工具类使用文档
先准备两个基础实体类,Student,Teacher。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private String id;
private String name;
private String email;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
private String id;
private String name;
private String emailAddress;
}
TC1,基础实体映射
/**
* 只拷贝相同的属性
*/
@Test
public void convertObject(){
Student student = new Student("1","javadaily","jianzh5@163.com");
Teacher teacher = OrikaUtils.convert(student, Teacher.class);
System.out.println(teacher);
}
输出结果:
Teacher(id=1, name=javadaily, emailAddress=null)
此时由于属性名不一致,无法映射字段email。
TC2,实体映射 - 字段转换
/**
* 拷贝不同属性
*/
@Test
public void convertRefObject(){
Student student = new Student("1","javadaily","jianzh5@163.com");
Map<string,string> refMap = new HashMap<>(1);
//map key 放置 源属性,value 放置 目标属性
refMap.put("email","emailAddress");
Teacher teacher = OrikaUtils.convert(student, Teacher.class, refMap);
System.out.println(teacher);
}
输出结果:
Teacher(id=1, name=javadaily, emailAddress=jianzh5@163.com)
此时由于对字段做了映射,可以将email映射到emailAddress。注意这里的refMap中key放置的是源实体的属性,而value放置的是目标实体的属性,不要弄反了。
TC3,基础集合映射
/**
* 只拷贝相同的属性集合
*/
@Test
public void convertList(){
Student student1 = new Student("1","javadaily","jianzh5@163.com");
Student student2 = new Student("2","JAVA日知录","jianzh5@xxx.com");
List<student> studentList = Lists.newArrayList(student1,student2);
List<teacher> teacherList = OrikaUtils.convertList(studentList, Teacher.class);
System.out.println(teacherList);
}
输出结果:
[Teacher(id=1, name=javadaily, emailAddress=null), Teacher(id=2, name=JAVA日知录, emailAddress=null)]
此时由于属性名不一致,集合中无法映射字段email。
TC4,集合映射 - 字段映射
/**
* 映射不同属性的集合
*/
@Test
public void convertRefList(){
Student student1 = new Student("1","javadaily","jianzh5@163.com");
Student student2 = new Student("2","JAVA日知录","jianzh5@xxx.com");
List<student> studentList = Lists.newArrayList(student1,student2);
Map<string,string> refMap = new HashMap<>(2);
//map key 放置 源属性,value 放置 目标属性
refMap.put("email","emailAddress");
List<teacher> teacherList = OrikaUtils.convertList(studentList, Teacher.class,refMap);
System.out.println(teacherList);
}
输出结果:
[Teacher(id=1, name=javadaily, emailAddress=jianzh5@163.com), Teacher(id=2, name=JAVA日知录, emailAddress=jianzh5@xxx.com)]
也可以通过这样映射:
Map<string,string> refMap = new HashMap<>(2);
refMap.put("email","emailAddress");
List<teacher> teacherList = OrikaUtils.classMap(Student.class,Teacher.class,refMap)
.mapAsList(studentList,Teacher.class);
TC5,集合与实体映射
有时候我们需要将集合数据映射到实体中,如Person类
@Data
public class Person {
private List<string> nameParts;
}
现在需要将Person类nameParts的值映射到Student中,可以这样做
/**
* 数组和List的映射
*/
@Test
public void convertListObject(){
Person person = new Person();
person.setNameParts(Lists.newArrayList("1","javadaily","jianzh5@163.com"));
Map<string,string> refMap = new HashMap<>(2);
//map key 放置 源属性,value 放置 目标属性
refMap.put("nameParts[0]","id");
refMap.put("nameParts[1]","name");
refMap.put("nameParts[2]","email");
Student student = OrikaUtils.convert(person, Student.class,refMap);
System.out.println(student);
}
输出结果:
Student(id=1, name=javadaily, email=jianzh5@163.com)
TC6,类类型映射
有时候我们需要类类型对象映射,如BasicPerson类
@Data
public class BasicPerson {
private Student student;
}
现在需要将BasicPerson映射到Teacher
/**
* 类类型映射
*/
@Test
public void convertClassObject(){
BasicPerson basicPerson = new BasicPerson();
Student student = new Student("1","javadaily","jianzh5@163.com");
basicPerson.setStudent(student);
Map<string,string> refMap = new HashMap<>(2);
//map key 放置 源属性,value 放置 目标属性
refMap.put("student.id","id");
refMap.put("student.name","name");
refMap.put("student.email","emailAddress");
Teacher teacher = OrikaUtils.convert(basicPerson, Teacher.class,refMap);
System.out.println(teacher);
}
输出结果:
Teacher(id=1, name=javadaily, emailAddress=jianzh5@163.com)
TC7,多重映射
有时候我们会遇到多重映射,如将StudentGrade映射到TeacherGrade
@Data
public class StudentGrade {
private String studentGradeName;
private List<student> studentList;
}
@Data
public class TeacherGrade {
private String teacherGradeName;
private List<teacher> teacherList;
}
这种场景稍微复杂,Student与Teacher的属性有email字段不相同,需要做转换映射;StudentGrade与TeacherGrade中的属性也需要映射。
/**
* 一对多映射
*/
@Test
public void convertComplexObject(){
Student student1 = new Student("1","javadaily","jianzh5@163.com");
Student student2 = new Student("2","JAVA日知录","jianzh5@xxx.com");
List<student> studentList = Lists.newArrayList(student1,student2);
StudentGrade studentGrade = new StudentGrade();
studentGrade.setStudentGradeName("硕士");
studentGrade.setStudentList(studentList);
Map<string,string> refMap1 = new HashMap<>(1);
//map key 放置 源属性,value 放置 目标属性
refMap1.put("email","emailAddress");
OrikaUtils.register(Student.class,Teacher.class,refMap1);
Map<string,string> refMap2 = new HashMap<>(2);
//map key 放置 源属性,value 放置 目标属性
refMap2.put("studentGradeName", "teacherGradeName");
refMap2.put("studentList", "teacherList");
TeacherGrade teacherGrade = OrikaUtils.convert(studentGrade,TeacherGrade.class,refMap2);
System.out.println(teacherGrade);
}
多重映射的场景需要根据情况调用OrikaUtils.register()注册字段映射。
输出结果:
TeacherGrade(teacherGradeName=硕士, teacherList=[Teacher(id=1, name=javadaily, emailAddress=jianzh5@163.com), Teacher(id=2, name=JAVA日知录, emailAddress=jianzh5@xxx.com)])
TC8,MyBaits plus分页映射
如果你使用的是mybatis的分页组件,可以这样转换
public IPage<userdto> selectPage(UserDTO userDTO, Integer pageNo, Integer pageSize) {
Page page = new Page<>(pageNo, pageSize);
LambdaQueryWrapper<user> query = new LambdaQueryWrapper();
if (StringUtils.isNotBlank(userDTO.getName())) {
query.like(User::getKindName,userDTO.getName());
}
IPage<user> pageList = page(page,query);
// 实体转换 SysKind转化为SysKindDto
Map<string,string> refMap = new HashMap<>(3);
refMap.put("kindName","name");
refMap.put("createBy","createUserName");
refMap.put("createTime","createDate");
return pageList.convert(item -> OrikaUtils.convert(item, UserDTO.class, refMap));
}
小结
在MVC架构中肯定少不了需要用到对象复制,属性转换的功能,借用Orika组件,可以很简单实现这些功能。本文在Orika的基础上封装了工具类,进一步简化了Orika的操作,希望对各位有所帮助。
最后,我是飘渺Jam,一名写代码的架构师,做架构的程序员,期待您的转发与关注,当然也可以添加我的个人微信 jianzh5,咱们一起聊技术!
老鸟系列源码已经上传至GitHub,需要的在公号【JAVA日知录】回复关键字 0923 获取
SpringBoot 如何进行对象复制,老鸟们都这么玩的!的更多相关文章
- SpringBoot 如何生成接口文档,老鸟们都这么玩的!
大家好,我是飘渺. SpringBoot老鸟系列的文章已经写了两篇,每篇的阅读反响都还不错,果然大家还是对SpringBoot比较感兴趣.那今天我们就带来老鸟系列的第三篇:集成Swagger接口文档以 ...
- SpringBoot 如何进行参数校验,老鸟们都这么玩的!
大家好,我是飘渺. 前几天写了一篇 SpringBoot如何统一后端返回格式?老鸟们都是这样玩的! 阅读效果还不错,而且被很多号主都转载过,今天我们继续第二篇,来聊聊在SprinBoot中如何集成参数 ...
- SpringBoot 如何实现异步编程,老鸟们都这么玩的!
镜像下载.域名解析.时间同步请点击 阿里巴巴开源镜像站 首先我们来看看在Spring中为什么要使用异步编程,它能解决什么问题? 为什么要用异步框架,它解决什么问题? 在SpringBoot的日常开发中 ...
- SpringBoot 如何进行限流?老鸟们都这么玩的!
大家好,我是飘渺.SpringBoot老鸟系列的文章已经写了四篇,每篇的阅读反响都还不错,那今天继续给大家带来老鸟系列的第五篇,来聊聊在SpringBoot项目中如何对接口进行限流,有哪些常见的限流算 ...
- JS对象复制
在JavaScript很多人复制一个对象的时候都是直接用"=",因为大家都觉得脚本语言是没有指针.引用.地址之类的,所以直接用"="就可以把一个对象复制给另外一 ...
- 对象复制、克隆、深度clone
-------------------------------------------------------------------------------- ------------------- ...
- Java反射 - 2(对象复制,父类域,内省)
为什么要复制对象?假设有个类Car,包含name,color2个属性,那么将car1对象复制给car2对象,只需要car2.setName(car1.getName)与car2.setColor(ca ...
- JS对象复制(深拷贝、浅拷贝)
如何在 JS 中复制对象 在本文中,我们将从浅拷贝(shallow copy)和深拷贝(deep copy)两个方面,介绍多种 JS 中复制对象的方法. 在开始之前,有一些基础知识值得一提:Javas ...
- JavaScript对象复制(一)(转载)
在JavaScript很多人复制一个对象的时候都是直接用"=",因为大家都觉得脚本语言是没有指针.引用.地址之类的,所以直接用"="就可以把一个对象复制给另外一 ...
随机推荐
- Kurento实战之二:快速部署和体验
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- sqli-labs lesson 11-15
从这一关开始我们开始进入到post注入的世界了,什么是post呢?就是数据从客户端提交到服务器端,例如我们在登录过程中,输入用户名和密码,用户名和密码以表单的形式提交,提交到服务器后服务器再进行验证. ...
- 当任意文件上传偶遇Safedog
0x01 写在前面 渗透过程中可能会经常遭遇WAF,此时不要轻易放弃,绞尽脑汁竭尽全力,或许弹尽粮绝之时也是柳暗花明之日. 0x02 过狗上传 一次项目渗透过程中,找个一处上传功能 先上传图片,测试上 ...
- Git(12)-- Git 分支 - 分支简介
@ 目录 1.分支简介 1.1.初始化并首次提交 首次提交对象及其树结构: git 的 cat-file 的命令用法: 1.2.修改并第二次提交 第二次提交对象及其树结构: 1.3.修改并第三次提交 ...
- Kurento实战之四:应用开发指南
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- asp.net core 声明controller的方法
1, 对类名直接添加Controller, 如TestController. 2, 继承Controller 类. 3, 对类名添加controller的属性, 如[Controller]
- C++ Opencv图像直方图
Mat image = imread("D:/ju.jpg"); imshow("素材图", image); int bins = 256; //直条为256 ...
- HuaWeiJava 上机
1 /* 2 * 第二题,输入字符串长度,字符串,计数m.从前往后计数,当数到m个元素时,m个元素出列,同时将该元素赋值给m, 3 * 然后从下一个数计数循环,直到所有数字都出列,给定的数全部为大于0 ...
- 基于 Mysql 实现一个简易版搜索引擎
前言 前段时间,因为项目需求,需要根据关键词搜索聊天记录,这不就是一个搜索引擎的功能吗? 于是我第一时间想到的就是 ElasticSearch 分布式搜索引擎,但是由于一些原因,公司的服务器资源比较紧 ...
- SpringBoot详解(一)——
https://www.cnblogs.com/lifullmoon/p/14957771.html https://www.cnblogs.com/lifullmoon/p/14957751.htm ...