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


    大家好,我是飘渺。

    今天带来SpringBoot老鸟系列的第四篇,来聊聊在日常开发中如何优雅的实现对象复制。

    首先我们看看为什么需要对象复制?

    为什么需要对象复制

    image-20210906145134173

    如上,是我们平时开发中最常见的三层MVC架构模型,编辑操作时Controller层接收到前端传来的DTO对象,在Service层需要将DTO转换成DO,然后在数据库中保存。查询操作时Service层查询到DO对象后需要将DO对象转换成VO对象,然后通过Controller层返回给前端进行渲染。

    这中间会涉及到大量的对象转换,很明显我们不能直接使用getter/setter复制对象属性,这看上去太low了。想象一下你业务逻辑中充斥着大量的getter&setter,代码评审时老鸟们会如何笑话你?

    image-20210716084136689

    所以我们必须要找一个第三方工具来帮我们实现对象转换。

    看到这里有同学可能会问,为什么不能前后端都统一使用DO对象呢?这样就不存在对象转换呀?

    设想一下如果我们不想定义 DTO 和 VO,直接将 DO 用到数据访问层、服务层、控制层和外部访问接口上。此时该表删除或则修改一个字段,DO 必须同步修改,这种修改将会影响到各层,这并不符合高内聚低耦合的原则。通过定义不同的 DTO 可以控制对不同系统暴露不同的属性,通过属性映射还可以实现具体的字段名称的隐藏。不同业务使用不同的模型,当一个业务发生变更需要修改字段时,不需要考虑对其它业务的影响,如果使用同一个对象则可能因为 “不敢乱改” 而产生很多不优雅的兼容性行为。

    对象复制工具类推荐

    对象复制的类库工具有很多,除了常见的Apache的BeanUtils,Spring的BeanUtilsCglib BeanCopier,还有重量级组件MapStructOrikaDozerModelMapper等。

    如果没有特殊要求,这些工具类都可以直接使用,除了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 PersonDest
    TargetClass target = mapper.map(source, TargetClass.class);  
    

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

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

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

    它提供了五个公共方法:

    image-20210903151829872

    分别对应:

    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)
    

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

  • 相关阅读:
    JS中所有数组的方法和所有的对象方法,包含ES5和ES6
    (原创!)彻底理解JS中的事件,事件处理函数,钩子函数,回调函数。
    深拷贝和浅拷贝的区别,object.assgin方法是深拷贝还是浅拷贝
    axios,Ajax,jQuery ajax,axios和fetch的区别
    python : 文档比较
    leetcode——989.数组形式的整数加法
    leetcode——66.加一
    leetcode——283.移动0
    leetcode——27.移除元素
    leetcode刷题第一天——两数之和
  • 原文地址:https://www.cnblogs.com/jianzh5/p/15324151.html
Copyright © 2020-2023  润新知