写点什么

SpringBoot 如何进行对象复制,老鸟们都这么玩的

用户头像
胧月
关注
发布于: 1 小时前

今天带来 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


强制规定避免使用 Apache BeanUtils

至于剩下的重量级组件,综合考虑其性能还有使用的易用性,我这里更推荐使用 Orika。Orika 底层采用了 javassist 类库生成 Bean 映射的字节码,之后直接加载执行生成的字节码文件,在速度上比使用反射进行赋值会快很多。

国外大神 baeldung 已经对常见的组件性能进行过详细测试,大家可以通过 https://www.baeldung.com/java-performance-mapping-frameworks 查看。

Orika 基本使用

要使用 Orika 很简单,只需要简单四步:

  1. 引入依赖

<dependency>  <groupId>ma.glasnost.orika</groupId>  <artifactId>orika-core</artifactId>  <version>1.5.4</version></dependency>
复制代码
  1. 构造一个 MapperFactory

MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();  
复制代码
  1. 注册字段映射

mapperFactory.classMap(SourceClass.class, TargetClass.class)     .field("firstName", "givenName")   .field("lastName", "sirName")   .byDefault()   .register();
复制代码

当字段名在两个实体不一致时可以通过.field()方法进行映射,如果字段名都一样则可省略,byDefault()方法用于注册名称相同的属性,如果不希望某个字段参与映射,可以使用 exclude 方法。

  1. 进行映射

MapperFacade mapper = mapperFactory.getMapperFacade();
SourceClass source = new SourceClass(); // set some field values...// map the fields of 'source' onto a new instance of PersonDestTargetClass target = mapper.map(source, TargetClass.class);
复制代码

经过上面四步我们就完成了 SourceClass 到 TargetClass 的转换。至于 Orika 的其他使用方法大家可以参考 http://orika-mapper.github.io/orika-docs/index.html

看到这里,肯定有粉丝会说:你这推荐的啥玩意呀,这个 Orika 使用也不简单呀,每次都要这先创建 MapperFactory,建立字段映射关系,才能进行映射转换。

别急,我这里给你准备了一个工具类 OrikaUtils,你可以通过文末 github 仓库获取。

它提供了五个公共方法:


分别对应:

  1. 字段一致实体转换

  2. 字段不一致实体转换(需要字段映射)

  3. 字段一致集合转换

  4. 字段不一致集合转换(需要字段映射)

  5. 字段属性转换注册

接下来我们通过单元测试案例重点介绍此工具类的使用。

Orika 工具类使用文档

先准备两个基础实体类,Student,Teacher。

@Data@AllArgsConstructor@NoArgsConstructorpublic class Student {    private String id;    private String name;    private String email;}

复制代码


@Data@AllArgsConstructor@NoArgsConstructorpublic class Teacher {    private String id;    private String name;    private String emailAddress;}
复制代码

TC1,基础实体映射

/** * 只拷贝相同的属性 */@Testpublic 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,实体映射 - 字段转换

/** * 拷贝不同属性 */@Testpublic 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,基础集合映射

/**  * 只拷贝相同的属性集合  */@Testpublic 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,集合映射 - 字段映射

/** * 映射不同属性的集合 */@Testpublic 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 类

@Datapublic class Person {    private List<String> nameParts;}
复制代码

现在需要将 Person 类 nameParts 的值映射到 Student 中,可以这样做

/** * 数组和List的映射 */@Testpublic 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 类

@Datapublic class BasicPerson {    private Student student;}
复制代码

现在需要将 BasicPerson 映射到 Teacher

/** * 类类型映射 */@Testpublic 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

@Datapublic class StudentGrade {    private String studentGradeName;    private List<Student> studentList;}
@Datapublic class TeacherGrade { private String teacherGradeName; private List<Teacher> teacherList;}
复制代码

这种场景稍微复杂,Student 与 Teacher 的属性有 email 字段不相同,需要做转换映射;StudentGrade 与 TeacherGrade 中的属性也需要映射。

/** * 一对多映射 */@Testpublic 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 的操作,希望对各位有所帮助。

老鸟系列 github 仓库:https://github.com/jianzh5/cloud-blog/


来源:https://mp.weixin.qq.com/s/WHdyuCF0GGiC6wJLBIKaFQ

用户头像

胧月

关注

还未添加个人签名 2021.08.17 加入

还未添加个人简介

评论

发布
暂无评论
SpringBoot 如何进行对象复制,老鸟们都这么玩的