SpringBoot 进行对象复制--Orika
业务开发需要对象复制
如上,是我们平时开发中最常见的三层MVC架构模型,编辑操作时Controller层接收到前端传来的DTO对象,在Service层需要将DTO转换成DO,然后在数据库中保存。查询操作时Service层查询到DO对象后需要将DO对象转换成VO对象,然后通过Controller层返回给前端进行渲染。
这中间会涉及到大量的对象转换,很明显我们不能直接使用getter/setter复制对象属性,严重影响开发效率
对象复制工具类推荐
对象复制的类库工具有很多,除了常见的Apache的BeanUtils,Spring的BeanUtils,Cglib BeanCopier,还有重量级组件MapStruct,Orika,Dozer,ModelMapper等。
如果没有特殊要求,这些工具类都可以直接使用,除了Apache的BeanUtils。原因在于Apache BeanUtils底层源码为了追求完美,加了过多的包装,使用了很多反射,做了很多校验,所以导致性能较差,并在阿里巴巴开发手册上强制规定避免使用 Apache BeanUtils。
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方法。
OrikaUtils
package com.jianzh5.blog.orika;
import ma.glasnost.orika.MapperFacade;
import ma.glasnost.orika.MapperFactory;
import ma.glasnost.orika.impl.DefaultMapperFactory;
import ma.glasnost.orika.metadata.ClassMapBuilder;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Description:
* Orika封装的工具类
* @author babayang
* @date 2022/6/25 14:01
*/
public class OrikaUtils {
private static final MapperFactory FACTORY = new DefaultMapperFactory.Builder().build();
/**
* 缓存实例集合
*/
private static final Map<String, MapperFacade> CACHE_MAPPER = new ConcurrentHashMap<>();
private final MapperFacade mapper;
public OrikaUtils(MapperFacade mapper) {
this.mapper = mapper;
}
/**
* 转换实体函数
* @param sourceEntity 源实体
* @param targetClass 目标类对象
* @param refMap 配置源类与目标类不同字段名映射
* @param <S> 源泛型
* @param <T> 目标泛型
* @return 目标实体
*/
public static <S, T> T convert(S sourceEntity, Class<T> targetClass, Map<String, String> refMap) {
if (sourceEntity == null) {
return null;
}
return classMap(sourceEntity.getClass(), targetClass, refMap).map(sourceEntity, targetClass);
}
/**
* 转换实体函数
*
* @param sourceEntity 源实体
* @param targetClass 目标类对象
* @param <S> 源泛型
* @param <T> 目标泛型
* @return 目标实体
*/
public static <S, T> T convert(S sourceEntity, Class<T> targetClass) {
return convert(sourceEntity, targetClass, null);
}
/**
* 转换实体集合函数
*
* @param sourceEntityList 源实体集合
* @param targetClass 目标类对象
* @param refMap 配置源类与目标类不同字段名映射
* @param <S> 源泛型
* @param <T> 目标泛型
* @return 目标实体集合
*/
public static <S, T> List<T> convertList(List<S> sourceEntityList, Class<T> targetClass, Map<String, String> refMap) {
if (sourceEntityList == null) {
return null;
}
if (sourceEntityList.size() == 0) {
return new ArrayList<>(0);
}
return classMap(sourceEntityList.get(0).getClass(), targetClass, refMap).mapAsList(sourceEntityList, targetClass);
}
/**
* 转换实体集合函数
*
* @param sourceEntityList 源实体集合
* @param targetClass 目标类对象
* @param <S> 源泛型
* @param <T> 目标泛型
* @return 目标实体集合
*/
public static <S, T> List<T> convertList(List<S> sourceEntityList, Class<T> targetClass) {
return convertList(sourceEntityList, targetClass, null);
}
/**
* 注册属性
* @param source 源类
* @param target 目标类
* @param refMap 属性转换
*/
public static <V, P> void register(Class<V> source, Class<P> target,Map<String, String> refMap){
if (CollectionUtils.isEmpty(refMap)) {
FACTORY.classMap(source, target).byDefault().register();
} else {
ClassMapBuilder<V, P> classMapBuilder = FACTORY.classMap(source, target);
refMap.forEach(classMapBuilder::field);
classMapBuilder.byDefault().register();
}
}
/**
* 属性名称一致可用
* @param source 源数据
* @param target 目标对象
* @return OrikaUtils
*/
private static <V, P> OrikaUtils classMap(Class<V> source, Class<P> target) {
return classMap(source, target, null);
}
/**
* 属性名称不一致可用
*
* @param source 原对象
* @param target 目标对象
* @return OrikaUtils
*/
private static synchronized <V, P> OrikaUtils classMap(Class<V> source, Class<P> target, Map<String, String> refMap) {
String key = source.getCanonicalName() + ":" + target.getCanonicalName();
if (CACHE_MAPPER.containsKey(key)) {
return new OrikaUtils(CACHE_MAPPER.get(key));
}
register(source,target,refMap);
MapperFacade mapperFacade = FACTORY.getMapperFacade();
CACHE_MAPPER.put(key, mapperFacade);
return new OrikaUtils(mapperFacade);
}
/**
* Orika复制对象
* @param source 源数据
* @param target 目标对象
* @return target
*/
private <V, P> P map(V source, Class<P> target) {
return mapper.map(source, target);
}
/**
* 复制List
* @param source 源对象
* @param target 目标对象
* @return P
*/
private <V, P> List<P> mapAsList(List<V> source, Class<P> target) {
return CollectionUtils.isEmpty(source) ? Collections.emptyList() : mapper.mapAsList(source, target);
}
}
分别对应:
- 字段一致实体转换
- 字段不一致实体转换(需要字段映射)
- 字段一致集合转换字段不一致集合转换(需要字段映射)
- 字段属性转换注册
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","李四","laoying@163.com");
Teacher teacher = OrikaUtils.convert(student, Teacher.class);
System.out.println(teacher);
}
输出结果:
Teacher(id=1, name=李四, emailAddress=null)
此时由于属性名不一致,无法映射字段email。
TC2,实体映射 - 字段转换
/**
* 拷贝不同属性
*/
@Test
public void convertRefObject(){
Student student = new Student("1","张三","babayang@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=张三, emailAddress=babayang@163.com)
此时由于对字段做了映射,可以将email映射到emailAddress。注意这里的refMap中key放置的是源实体的属性,而value放置的是目标实体的属性,不要弄反了。
TC3,基础集合映射
/**
* 只拷贝相同的属性集合
*/
@Test
public void convertList(){
Student student1 = new Student("1","张三","babayang@163.com");
Student student2 = new Student("2","李四","laoying@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=张三, emailAddress=null), Teacher(id=2, name=李四, emailAddress=null)]
此时由于属性名不一致,集合中无法映射字段email。
TC4,集合映射 - 字段映射
/**
* 映射不同属性的集合
*/
@Test
public void convertRefList(){
Student student1 = new Student("1","张三","babayang@163.com");
Student student2 = new Student("2","李四","laoying@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=张三, emailAddress=babayang@163.com), Teacher(id=2, name=李四, emailAddress=laoying@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","美丽","meili@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=美丽, email=meili@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","艾伦","ailun@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=艾伦, emailAddress=ailun@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","zhangsan","babayang@163.com");
Student student2 = new Student("2","lisi","laoying@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=zhangsan, emailAddress=babayang@163.com), Teacher(id=2, name=lisi, emailAddress=laoying@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));
}
参考文章: http://orika-mapper.github.io/orika-docs/index.html
参考文章: https://zhuanlan.zhihu.com/p/410630612
参考文章: https://github.com/jianzh5/cloud-blog