Java
在日常开发中如何优雅的实现对象复制。
首先看看为什么需要对象复制?

为什么需要对象复制

对象拷贝工具Orika的基本使用 - 图1
如上,是平时开发中最常见的三层MVC架构模型,编辑操作时Controller层接收到前端传来的DTO对象,在Service层需要将DTO转换成DO,然后在数据库中保存。查询操作时Service层查询到DO对象后需要将DO对象转换成VO对象,然后通过Controller层返回给前端进行渲染。
这中间会涉及到大量的对象转换,很明显不能直接使用getter/setter复制对象属性,这看上去太low了。
所以必须要找一个第三方工具来实现对象转换。
看到这里有同学可能会问,为什么不能前后端都统一使用DO对象呢?这样就不存在对象转换呀?
设想一下如果不想定义 DTO 和 VO,直接将 DO 用到数据访问层、服务层、控制层和外部访问接口上。此时该表删除或则修改一个字段,DO 必须同步修改,这种修改将会影响到各层,这并不符合高内聚低耦合的原则。通过定义不同的 DTO 可以控制对不同系统暴露不同的属性,通过属性映射还可以实现具体的字段名称的隐藏。不同业务使用不同的模型,当一个业务发生变更需要修改字段时,不需要考虑对其它业务的影响,如果使用同一个对象则可能因为 “不敢乱改” 而产生很多不优雅的兼容性行为。

对象复制工具类推荐

对象复制的类库工具有很多,除了常见的Apache的BeanUtils,Spring的BeanUtils,Cglib BeanCopier,还有重量级组件MapStruct,Orika,Dozer,ModelMapper等。
如果没有特殊要求,这些工具类都可以直接使用,除了Apache的BeanUtils。原因在于Apache BeanUtils底层源码为了追求完美,加了过多的包装,使用了很多反射,做了很多校验,所以导致性能较差,并在阿里巴巴开发手册上强制规定避免使用 Apache BeanUtils。
对象拷贝工具Orika的基本使用 - 图2
强制规定避免使用 Apache BeanUtils
至于剩下的重量级组件,综合考虑其性能还有使用的易用性,这里更推荐使用Orika。Orika底层采用了javassist类库生成Bean映射的字节码,之后直接加载执行生成的字节码文件,在速度上比使用反射进行赋值会快很多。
国外大神 baeldung 已经对常见的组件性能进行过详细测试,大家可以通过 https://www.baeldung.com/java-performance-mapping-frameworks 查看。

Orika基本使用

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

引入依赖

  1. <dependency>
  2. <groupId>ma.glasnost.orika</groupId>
  3. <artifactId>orika-core</artifactId>
  4. <version>1.5.4</version>
  5. </dependency>

构造一个MapperFactory

  1. MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();

注册字段映射

  1. mapperFactory.classMap(SourceClass.class, TargetClass.class)
  2. .field("firstName", "givenName")
  3. .field("lastName", "sirName")
  4. .byDefault()
  5. .register();

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

进行映射

  1. MapperFacade mapper = mapperFactory.getMapperFacade();
  2. SourceClass source = new SourceClass();
  3. // set some field values
  4. ...
  5. // map the fields of 'source' onto a new instance of PersonDest
  6. TargetClass target = mapper.map(source, TargetClass.class);

经过上面四步就完成了SourceClass到TargetClass的转换。至于Orika的其他使用方法大家可以参考 http://orika-mapper.github.io/orika-docs/index.html
每次都要这先创建MapperFactory,建立字段映射关系,才能进行映射转换。
别急,这里准备了一个工具类OrikaUtils。
提供了五个公共方法:

  1. import ma.glasnost.orika.MapperFacade;
  2. import ma.glasnost.orika.MapperFactory;
  3. import ma.glasnost.orika.impl.DefaultMapperFactory;
  4. import ma.glasnost.orika.metadata.ClassMapBuilder;
  5. import org.springframework.util.CollectionUtils;
  6. import java.util.ArrayList;
  7. import java.util.Collections;
  8. import java.util.List;
  9. import java.util.Map;
  10. import java.util.concurrent.ConcurrentHashMap;
  11. /**
  12. * Description:
  13. * Orika封装的工具类
  14. * @date 2021/9/2 14:01
  15. */
  16. public class OrikaUtils {
  17. private static final MapperFactory FACTORY = new DefaultMapperFactory.Builder().build();
  18. /**
  19. * 缓存实例集合
  20. */
  21. private static final Map<String, MapperFacade> CACHE_MAPPER = new ConcurrentHashMap<>();
  22. private final MapperFacade mapper;
  23. public OrikaUtils(MapperFacade mapper) {
  24. this.mapper = mapper;
  25. }
  26. /**
  27. * 转换实体函数
  28. * @param sourceEntity 源实体
  29. * @param targetClass 目标类对象
  30. * @param refMap 配置源类与目标类不同字段名映射
  31. * @param <S> 源泛型
  32. * @param <T> 目标泛型
  33. * @return 目标实体
  34. */
  35. public static <S, T> T convert(S sourceEntity, Class<T> targetClass, Map<String, String> refMap) {
  36. if (sourceEntity == null) {
  37. return null;
  38. }
  39. return classMap(sourceEntity.getClass(), targetClass, refMap).map(sourceEntity, targetClass);
  40. }
  41. /**
  42. * 转换实体函数
  43. *
  44. * @param sourceEntity 源实体
  45. * @param targetClass 目标类对象
  46. * @param <S> 源泛型
  47. * @param <T> 目标泛型
  48. * @return 目标实体
  49. */
  50. public static <S, T> T convert(S sourceEntity, Class<T> targetClass) {
  51. return convert(sourceEntity, targetClass, null);
  52. }
  53. /**
  54. * 转换实体集合函数
  55. *
  56. * @param sourceEntityList 源实体集合
  57. * @param targetClass 目标类对象
  58. * @param refMap 配置源类与目标类不同字段名映射
  59. * @param <S> 源泛型
  60. * @param <T> 目标泛型
  61. * @return 目标实体集合
  62. */
  63. public static <S, T> List<T> convertList(List<S> sourceEntityList, Class<T> targetClass, Map<String, String> refMap) {
  64. if (sourceEntityList == null) {
  65. return null;
  66. }
  67. if (sourceEntityList.size() == 0) {
  68. return new ArrayList<>(0);
  69. }
  70. return classMap(sourceEntityList.get(0).getClass(), targetClass, refMap).mapAsList(sourceEntityList, targetClass);
  71. }
  72. /**
  73. * 转换实体集合函数
  74. *
  75. * @param sourceEntityList 源实体集合
  76. * @param targetClass 目标类对象
  77. * @param <S> 源泛型
  78. * @param <T> 目标泛型
  79. * @return 目标实体集合
  80. */
  81. public static <S, T> List<T> convertList(List<S> sourceEntityList, Class<T> targetClass) {
  82. return convertList(sourceEntityList, targetClass, null);
  83. }
  84. /**
  85. * 注册属性
  86. * @param source 源类
  87. * @param target 目标类
  88. * @param refMap 属性转换
  89. */
  90. public static <V, P> void register(Class<V> source, Class<P> target,Map<String, String> refMap){
  91. if (CollectionUtils.isEmpty(refMap)) {
  92. FACTORY.classMap(source, target).byDefault().register();
  93. } else {
  94. ClassMapBuilder<V, P> classMapBuilder = FACTORY.classMap(source, target);
  95. refMap.forEach(classMapBuilder::field);
  96. classMapBuilder.byDefault().register();
  97. }
  98. }
  99. /**
  100. * 属性名称一致可用
  101. * @param source 源数据
  102. * @param target 目标对象
  103. * @return OrikaUtils
  104. */
  105. private static <V, P> OrikaUtils classMap(Class<V> source, Class<P> target) {
  106. return classMap(source, target, null);
  107. }
  108. /**
  109. * 属性名称不一致可用
  110. *
  111. * @param source 原对象
  112. * @param target 目标对象
  113. * @return OrikaUtils
  114. */
  115. private static synchronized <V, P> OrikaUtils classMap(Class<V> source, Class<P> target, Map<String, String> refMap) {
  116. String key = source.getCanonicalName() + ":" + target.getCanonicalName();
  117. if (CACHE_MAPPER.containsKey(key)) {
  118. return new OrikaUtils(CACHE_MAPPER.get(key));
  119. }
  120. register(source,target,refMap);
  121. MapperFacade mapperFacade = FACTORY.getMapperFacade();
  122. CACHE_MAPPER.put(key, mapperFacade);
  123. return new OrikaUtils(mapperFacade);
  124. }
  125. /**
  126. * Orika复制对象
  127. * @param source 源数据
  128. * @param target 目标对象
  129. * @return target
  130. */
  131. private <V, P> P map(V source, Class<P> target) {
  132. return mapper.map(source, target);
  133. }
  134. /**
  135. * 复制List
  136. * @param source 源对象
  137. * @param target 目标对象
  138. * @return P
  139. */
  140. private <V, P> List<P> mapAsList(List<V> source, Class<P> target) {
  141. return CollectionUtils.isEmpty(source) ? Collections.emptyList() : mapper.mapAsList(source, target);
  142. }
  143. }

分别对应:

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

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

Orika工具类使用文档

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

  1. @Data
  2. @AllArgsConstructor
  3. @NoArgsConstructor
  4. public class Student {
  5. private String id;
  6. private String name;
  7. private String email;
  8. }
  9. @Data
  10. @AllArgsConstructor
  11. @NoArgsConstructor
  12. public class Teacher {
  13. private String id;
  14. private String name;
  15. private String emailAddress;
  16. }

TC1,基础实体映射

  1. /**
  2. * 只拷贝相同的属性
  3. */
  4. @Test
  5. public void convertObject(){
  6. Student student = new Student("1","java","fcscanf@outlook.com");
  7. Teacher teacher = OrikaUtils.convert(student, Teacher.class);
  8. System.out.println(teacher);
  9. }

输出结果:

  1. Teacher(id=1, name=java, emailAddress=null)

此时由于属性名不一致,无法映射字段email。

TC2,实体映射 - 字段转换

  1. /**
  2. * 拷贝不同属性
  3. */
  4. @Test
  5. public void convertRefObject(){
  6. Student student = new Student("1","java","fcscanf@outlook.com");
  7. Map<String,String> refMap = new HashMap<>(1);
  8. //map key 放置 源属性,value 放置 目标属性
  9. refMap.put("email","emailAddress");
  10. Teacher teacher = OrikaUtils.convert(student, Teacher.class, refMap);
  11. System.out.println(teacher);
  12. }

输出结果:

  1. Teacher(id=1, name=java, emailAddress=fcscanf@outlook.com)

此时由于对字段做了映射,可以将email映射到emailAddress。注意这里的refMap中key放置的是源实体的属性,而value放置的是目标实体的属性,不要弄反了。

TC3,基础集合映射

  1. /**
  2. * 只拷贝相同的属性集合
  3. */
  4. @Test
  5. public void convertList(){
  6. Student student1 = new Student("1","java","fcscanf@outlook.com");
  7. Student student2 = new Student("2","Fcant","fcscanf@outlook.com");
  8. List<Student> studentList = Lists.newArrayList(student1,student2);
  9. List<Teacher> teacherList = OrikaUtils.convertList(studentList, Teacher.class);
  10. System.out.println(teacherList);
  11. }

输出结果:

  1. [Teacher(id=1, name=java, emailAddress=null), Teacher(id=2, name=Fcant, emailAddress=null)]

此时由于属性名不一致,集合中无法映射字段email。

TC4,集合映射 - 字段映射

  1. /**
  2. * 映射不同属性的集合
  3. */
  4. @Test
  5. public void convertRefList(){
  6. Student student1 = new Student("1","java","fcscanf@outlook.com");
  7. Student student2 = new Student("2","Fcant","fcscanf@outlook.com");
  8. List<Student> studentList = Lists.newArrayList(student1,student2);
  9. Map<String,String> refMap = new HashMap<>(2);
  10. //map key 放置 源属性,value 放置 目标属性
  11. refMap.put("email","emailAddress");
  12. List<Teacher> teacherList = OrikaUtils.convertList(studentList, Teacher.class,refMap);
  13. System.out.println(teacherList);
  14. }

输出结果:

  1. [Teacher(id=1, name=java, emailAddress=fcscanf@outlook.com), Teacher(id=2, name=Fcant, emailAddress=fcscanf@outlook.com)]

也可以通过这样映射:

  1. Map<String,String> refMap = new HashMap<>(2);
  2. refMap.put("email","emailAddress");
  3. List<Teacher> teacherList = OrikaUtils.classMap(Student.class,Teacher.class,refMap)
  4. .mapAsList(studentList,Teacher.class);

TC5,集合与实体映射

有时候需要将集合数据映射到实体中,如Person类

  1. @Data
  2. public class Person {
  3. private List<String> nameParts;
  4. }

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

  1. /**
  2. * 数组和List的映射
  3. */
  4. @Test
  5. public void convertListObject(){
  6. Person person = new Person();
  7. person.setNameParts(Lists.newArrayList("1","java","fcscanf@outlook.com"));
  8. Map<String,String> refMap = new HashMap<>(2);
  9. //map key 放置 源属性,value 放置 目标属性
  10. refMap.put("nameParts[0]","id");
  11. refMap.put("nameParts[1]","name");
  12. refMap.put("nameParts[2]","email");
  13. Student student = OrikaUtils.convert(person, Student.class,refMap);
  14. System.out.println(student);
  15. }

输出结果:

  1. Student(id=1, name=java, email=fcscanf@outlook.com)

TC6,类类型映射

有时候需要类类型对象映射,如BasicPerson类

  1. @Data
  2. public class BasicPerson {
  3. private Student student;
  4. }

现在需要将BasicPerson映射到Teacher

  1. /**
  2. * 类类型映射
  3. */
  4. @Test
  5. public void convertClassObject(){
  6. BasicPerson basicPerson = new BasicPerson();
  7. Student student = new Student("1","java","fcscanf@outlook.com");
  8. basicPerson.setStudent(student);
  9. Map<String,String> refMap = new HashMap<>(2);
  10. //map key 放置 源属性,value 放置 目标属性
  11. refMap.put("student.id","id");
  12. refMap.put("student.name","name");
  13. refMap.put("student.email","emailAddress");
  14. Teacher teacher = OrikaUtils.convert(basicPerson, Teacher.class,refMap);
  15. System.out.println(teacher);
  16. }

输出结果:

  1. Teacher(id=1, name=java, emailAddress=fcscanf@outlook.com)

TC7,多重映射

有时候会遇到多重映射,如将StudentGrade映射到TeacherGrade

  1. @Data
  2. public class StudentGrade {
  3. private String studentGradeName;
  4. private List<Student> studentList;
  5. }
  6. @Data
  7. public class TeacherGrade {
  8. private String teacherGradeName;
  9. private List<Teacher> teacherList;
  10. }

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

  1. /**
  2. * 一对多映射
  3. */
  4. @Test
  5. public void convertComplexObject(){
  6. Student student1 = new Student("1","java","fcscanf@outlook.com");
  7. Student student2 = new Student("2","Fcant","fcscanf@outlook.com");
  8. List<Student> studentList = Lists.newArrayList(student1,student2);
  9. StudentGrade studentGrade = new StudentGrade();
  10. studentGrade.setStudentGradeName("硕士");
  11. studentGrade.setStudentList(studentList);
  12. Map<String,String> refMap1 = new HashMap<>(1);
  13. //map key 放置 源属性,value 放置 目标属性
  14. refMap1.put("email","emailAddress");
  15. OrikaUtils.register(Student.class,Teacher.class,refMap1);
  16. Map<String,String> refMap2 = new HashMap<>(2);
  17. //map key 放置 源属性,value 放置 目标属性
  18. refMap2.put("studentGradeName", "teacherGradeName");
  19. refMap2.put("studentList", "teacherList");
  20. TeacherGrade teacherGrade = OrikaUtils.convert(studentGrade,TeacherGrade.class,refMap2);
  21. System.out.println(teacherGrade);
  22. }

多重映射的场景需要根据情况调用OrikaUtils.register()注册字段映射。
输出结果:

  1. TeacherGrade(teacherGradeName=硕士, teacherList=[Teacher(id=1, name=java, emailAddress=fcscanf@outlook.com), Teacher(id=2, name=Fcant, emailAddress=fcscanf@outlook.com)])

TC8,MyBaits plus分页映射

如果使用的是mybatis的分页组件,可以这样转换

  1. public IPage<UserDTO> selectPage(UserDTO userDTO, Integer pageNo, Integer pageSize) {
  2. Page page = new Page<>(pageNo, pageSize);
  3. LambdaQueryWrapper<User> query = new LambdaQueryWrapper();
  4. if (StringUtils.isNotBlank(userDTO.getName())) {
  5. query.like(User::getKindName,userDTO.getName());
  6. }
  7. IPage<User> pageList = page(page,query);
  8. // 实体转换 SysKind转化为SysKindDto
  9. Map<String,String> refMap = new HashMap<>(3);
  10. refMap.put("kindName","name");
  11. refMap.put("createBy","createUserName");
  12. refMap.put("createTime","createDate");
  13. return pageList.convert(item -> OrikaUtils.convert(item, UserDTO.class, refMap));
  14. }

小结

在MVC架构中肯定少不了需要用到对象复制,属性转换的功能,借用Orika组件,可以很简单实现这些功能。