SpringBoot 如何进行对象复制-阿里云开发者社区

开发者社区> 飘渺Jam> 正文

SpringBoot 如何进行对象复制

简介: 首先我们看看为什么需要对象复制?
+关注继续查看

为什么需要对象复制


image.png如上,是我们平时开发中最常见的三层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。


9a29a12d34e98bcb3efe5e2087c7614a.png


至于剩下的重量级组件,综合考虑其性能还有使用的易用性,我这里更推荐使用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>
  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 PersonDest
TargetClass target = mapper.map(source, TargetClass.class);  


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


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


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


它提供了五个公共方法:


7f3f77c1557d6251edce9603a558f1a1.png


分别对应:

  1. 字段一致实体转换
  2. 字段不一致实体转换(需要字段映射)
  3. 字段一致集合转换
  4. 字段不一致集合转换(需要字段映射)
  5. 字段属性转换注册

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


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)
• 1

此时由于属性名不一致,无法映射字段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)

1

此时由于对字段做了映射,可以将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)]

1

此时由于属性名不一致,集合中无法映射字段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=jianz


也可以通过这样映射:


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)
• 1

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), 


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的操作,希望对各位有所帮助。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
WebService对象调用spring注解
WebService对象调用spring注解
6 0
Java反射获取基本类型、包装类型、String对象类型的字段值
package com.zkn.reflect.classinfo; import java.lang.reflect.Field; import java.lang.reflect.Type; import com.zkn.reflect.scope.Company; import com.zkn.reflect.scope.People; /** * &lt;p&gt;Titl
1769 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
7791 0
Spring容器创建对象的三种方式
/** * spring容器做的事情: * 解析spring的配置文件,利用Java的反射机制创建对象 * */ public class testHelloWorld { @Test public void testHe...
927 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
9521 0
Spring IOC 容器源码分析 - 创建原始 bean 对象
1. 简介 本篇文章是上一篇文章(创建单例 bean 的过程)的延续。在上一篇文章中,我们从战略层面上领略了doCreateBean方法的全过程。本篇文章,我们就从战术的层面上,详细分析doCreateBean方法中的一个重要的调用,即createBeanInstance方法。
1519 0
String str = new String(“abc“)到底new了几个对象
如果常量池里面没有“abc”这个字符串,那虚拟机就会在堆内存中new出一个String对象,还会在常量池中new一个abc字符串对象;
11700 0
Spring IOC 容器源码分析 - 填充属性到 bean 原始对象
1. 简介 本篇文章,我们来一起了解一下 Spring 是如何将配置文件中的属性值填充到 bean 对象中的。我在前面几篇文章中介绍过 Spring 创建 bean 的流程,即 Spring 先通过反射创建一个原始的 bean 对象,然后再向这个原始的 bean 对象中填充属性。
1605 0
+关注
飘渺Jam
飘渺Jam,CSDN博客专家, 一名写代码的架构师,做架构的程序员,可以通过 jianzh5 与我联系,咱们一起聊技术!
107
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载