前言
作为一个JAVA后端开发,日常工作中不免会经常用到对象拷贝,本篇就从实际案例出发,进行各种方案的实践对比。
场景重现
一日,糖哥接到需求,需要新写一个学生信息列表获取的接口,数据库的获取的方法底层已有封装,但是考虑到信息保密需要隐藏一部分敏感字段。现在我们来看下已有的StudentDO和新添加的StudentTO类。
@Data
Class StudentDO {
private Long id;
private String name;
private String idCard;
private String tel;
private Integer age;
}
@Data
Class StudentTO {
private Long id;
private String name;
private Integer age;
}
根据已有的方法可以获取到StudentDO的List,但是在实际输出时需要将其转换成StudentTO的List。
方案和思路
1.遍历然后get/set
这是最容易想到的办法。具体实现如下:
public List<StudentTO> copyList(List<StudentDO> doList) {
List<StudentTO> toList = new ArrayList<>();
for (StudentDO item : doList) {
StudentTO to = new StudentTO();
to.setId(item.getId());
to.setName(item.getName());
to.setAge(item.getAge());
toList.add(to);
}
return toList;
}
从代码性能来说,这种方式是最高效的,但是缺点是每次都要去基于不同的类实现不同的转换方法,在编码效率上是极低的。
2.反射实现通用性对象拷贝(Spring BeanUtils)
反射是java的一种特性,一般我们都会用它去解决一些通用性的问题。针对当前的问题,通用的解决思路就是将源对象与目标对象的相同属性值设置到目标对象中。
基于反射去实现对象拷贝有很多种,我们拿其中使用较为普遍的Spring BeanUtils举例。
我们先来看看基于Spring BeanUtils怎么解决上述问题。
public void studentCopyList(List<StudentDO> dolist) {
// spring BeanUtils实现
List<StudentTO> studentTOList1 = springBeanUtilsCopyList(dolist, StudentTO.class);
}
public static <T> List<T> springBeanUtilsCopyList(List<?> objects, Class<T> class1) {
try {
if (objects == null || objects.isEmpty()) {
return Collections.emptyList();
}
List<T> res = new ArrayList<>();
for (Object s : objects) {
T t = class1.newInstance();
BeanUtils.copyProperties(s, t);
res.add(t);
}
return res;
} catch (InstantiationException | IllegalAccessException e) {
return Collections.emptyList();
}
}
再来看看Spring BeanUtils的部分核心源码。
private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
可以看到其主要就是利用了反射机制,先遍历目标对象的属性值,当发现源对象中有相同属性值时进行设置。
这种做法的好处就是通用性很强,但是缺点是反射会降低性能,尤其在调用量大的时候越发明显。
3.即时编译实现对象拷贝(cglib BeanCopier)
我们知道java不仅仅是一门静态编译语言,还带有即时编译的特性。思路是我们可以根据入参来动态生成相应的get/set代码处理逻辑,并即时编译运行。
这里我们举例基于cglib实现的BeanCopier,该工具类目前也引入在spring的core包中。先来看看如何使用。
public void studentCopyList(List<StudentDO> dolist) {
// cglib BeanCopier实现
List<StudentTO> studentTOList2 = cglibBeanCopierCopyList(dolist, StudentTO.class);
}
public static <T> List<T> cglibBeanCopierCopyList(List<?> objects, Class<T> targetClass) {
try {
if (objects == null || objects.isEmpty()) {
return Collections.emptyList();
}
List<T> res = new ArrayList<>();
for (Object s : objects) {
T t = targetClass.newInstance();
BeanCopier copier = BeanCopier.create(s.getClass(), t.getClass(), false);
copier.copy(s, t, null);
res.add(t);
}
return res;
} catch (InstantiationException | IllegalAccessException e) {
return Collections.emptyList();
}
}
再来看看其源码实现,其主要逻辑可以参考生成class部分的代码:
public void generateClass(ClassVisitor v) {
Type sourceType = Type.getType(this.source);
Type targetType = Type.getType(this.target);
ClassEmitter ce = new ClassEmitter(v);
ce.begin_class(46, 1, this.getClassName(), BeanCopier.BEAN_COPIER, (Type[])null, "<generated>");
EmitUtils.null_constructor(ce);
CodeEmitter e = ce.begin_method(1, BeanCopier.COPY, (Type[])null);
PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(this.source);
PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(this.target);
Map names = new HashMap();
for(int i = 0; i < getters.length; ++i) {
names.put(getters[i].getName(), getters[i]);
}
Local targetLocal = e.make_local();
Local sourceLocal = e.make_local();
if (this.useConverter) {
e.load_arg(1);
e.checkcast(targetType);
e.store_local(targetLocal);
e.load_arg(0);
e.checkcast(sourceType);
e.store_local(sourceLocal);
} else {
e.load_arg(1);
e.checkcast(targetType);
e.load_arg(0);
e.checkcast(sourceType);
}
for(int i = 0; i < setters.length; ++i) {
PropertyDescriptor setter = setters[i];
PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName());
if (getter != null) {
MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod());
MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod());
if (this.useConverter) {
Type setterType = write.getSignature().getArgumentTypes()[0];
e.load_local(targetLocal);
e.load_arg(2);
e.load_local(sourceLocal);
e.invoke(read);
e.box(read.getSignature().getReturnType());
EmitUtils.load_class(e, setterType);
e.push(write.getSignature().getName());
e.invoke_interface(BeanCopier.CONVERTER, BeanCopier.CONVERT);
e.unbox_or_zero(setterType);
e.invoke(write);
} else if (compatible(getter, setter)) {
e.dup2();
e.invoke(read);
e.invoke(write);
}
}
}
e.return_value();
e.end_method();
ce.end_class();
}
逻辑可以看到和基于反射的spring BeanUtils是一致的,只是实现方式不同。(cglib主要是利用了 Asm 字节码技术)
该种方式即解决了日常使用的编码效率问题,又优化了整个执行过程中的性能损耗。
4.注解处理器实现对象拷贝(mapstruct)
java源码编译由以下3个过程组成
- 分析和输入到符号表
- 注解处理
- 语义分析和生成class文件
很多工具其实都会基于注解处理器来实现相应的功能,例如常用的lombok等。
本次介绍的mapstruct也是同样的原理。
使用mapstruct会比之前的两种方法多一个步骤就是需要创建一个interface类,具体实现如下:
@Resource
private StudentMapper studentMapper;
public void studentCopyList(List<StudentDO> dolist) {
// 基于mapstruct实现
List<StudentTO> studentTOList3 = studentMapper.toTOList(dolist);
}
对应需要创建的接口类:
@Mapper(componentModel = "spring")
public interface StudentMapper {
List<StudentTO> toTOList(List<StudentDO> doList);
}
在源码编译阶段,注解处理器根据@Mapper注解会自动生成StudentMapper对应的实现类。
@Component
public class StudentMapperImpl implements StudentMapper {
public StudentMapperImpl() {
}
public List<StudentTO> toTOList(List<StudentDO> doList) {
if (doList == null) {
return null;
} else {
List<StudentTO> list = new ArrayList(doList.size());
Iterator var3 = doList.iterator();
while(var3.hasNext()) {
StudentDO studentDO = (StudentDO)var3.next();
list.add(this.studentDOToStudentTO(studentDO));
}
return list;
}
}
protected StudentTO studentDOToStudentTO(StudentDO studentDO) {
if (studentDO == null) {
return null;
} else {
StudentTO studentTO = new StudentTO();
studentTO.setId(studentDO.getId());
studentTO.setName(studentDO.getName());
studentTO.setAge(studentDO.getAge());
return studentTO;
}
}
}
相较之下,mapstruct每次实现调用的复杂度上会高一点,但是从性能上看是最优的,最接近原生的get/set调用实现。
性能对比
参考上面的案例,按list中元素个数,单次拷贝的耗时(单位:ms)横向对比如下:
方案 | 10个 | 100个 | 10000个 | 1000000个 |
---|---|---|---|---|
Spring BeanUtils(反射) | 650 | 723 | 770 | 950 |
cglib BeanCopier(asm字节码技术) | 48 | 60 | 65 | 300 |
mapstruct(注解处理器) | 3 | 4 | 5 | 40 |
可以看到mapstruct确实是性能最好的。但是另外还发现基于反射实现的Spring BeanUtils并没有随着调用次数的增大而大大提升耗时,与预期不符。这个其实不难想象是spring做了一层缓存,对于同一个类会缓存住反射获取的信息,参考CachedIntrospectionResults中的代码。
总结
从综合角度来看,mapstruct是最优解,但是日常使用中如果对于性能要求并没有那么高,其实其他的方案也是可以选择的,毕竟可以实现更好的封装复用。
网友评论