1.前言
项目中经常会遇到对象copy的场景,如何安全高效便捷copy对象,自然成为提高开发效率与系统性能的关注点。
通常较为流行的对象copy方法有以下4个:
- net.sf.cglib.beans.BeanCopier.copy(Object from, Object to, Converter converter)
- org.springframework.beans.BeanUtils.copyProperties(Object source, Object target)
- org.apache.commons.beanutils.BeanUtils.copyProperties(Object dest, Object orig)
- org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig)
它们的实现原理及特性如下表所示:
Features | Cglib-BeanCopier.copy | Spring-BeanUtils.copyProperties | Apache-BeanUtils.copyProperties | Apache-PropertyUtils.copyProperties |
---|---|---|---|---|
实现原理 | cglib动态代理 | 反射 | 反射 | 反射 |
复制哪些属性 | 复制源对象和目标对象中相同名称的属性 | 复制源对象和目标对象中相同名称的属性 | 复制源对象和目标对象中相同名称的属性 | 复制源对象和目标对象中相同名称与类型的属性 |
是否有类型转换 | 有,需要自定义实现Converter接口的转换器 | 没有 | 有,API默认注册了常用的转换器,也可以通过ConvertUtils.register方法添加自定义转换器 | 没有,这也是性能比Apache-BeanUtils快的主要原因 |
类型不匹配处理方式 | useConverter=true时类型不匹配抛异常,不复制任何属性;useConverter=false时,只复制类型匹配属性 | 类型不匹配属性不复制;只复制类型相同或源属性类型派生自目标属性类型的属性 | 有类型转换器则进行类型转换,没有或转换错误时抛异常不再复制属性 | 抛异常,不复制任何属性 |
2.Spring BeanUtils.copyProperties源码分析
BeanUtils有3个对象copy的静态方法,源码如下:
public static void copyProperties(Object source, Object target) throws BeansException {
copyProperties(source, target, null, (String[]) null);
}
public static void copyProperties(Object source, Object target, Class<?> editable) throws BeansException {
copyProperties(source, target, editable, (String[]) null);
}
public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
copyProperties(source, target, null, ignoreProperties);
}
/**
* 复制对象
* @param source 源对象
* @param target 目标对象
* @param editable 要copy的属性所属的对象类型,可以是taget对象的类型或其父类/接口类型
* @param ignoreProperties 不需要copy的属性名称
*/
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");
/*
* 获取要copy的属性所属的对象类型。
* 如果target类型与editable相同,则copy target类型的所有属性;
* 如果target类型是editable的子类型,则copy editable类型的所有属性;
*/
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;
}
// 要copy的属性描述对象数组
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
// 忽略掉不需要copy的属性
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
// 如果源对象与目标对象属性类型不一致,则不copy属性
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.性能测试
下面是对四个对象copy方法进行性能测试的代码:
import java.util.Date;
import org.junit.Test;
import net.sf.cglib.beans.BeanCopier;
import net.sf.cglib.core.Converter;
/**
* 测试代码
*/
public class BeanCopierTest {
// copy对象数量
final int beanCount = 500000;
@Test
public void testCglibBeanCopier() {
long start = System.currentTimeMillis();
BeanCopier beanCopier = BeanCopier.create(UserEntity.class, UserResultDto.class, true);
for (int i = 1; i <= beanCount; i++) {
UserEntity entity = createUser(i);
UserResultDto dto = new UserResultDto();
beanCopier.copy(entity, dto, new Converter() {
@Override
public Object convert(Object value, @SuppressWarnings("rawtypes") Class target, Object context) {
// 自定义转换逻辑
return value;
}
});
}
System.out.println("CglibBeanCopier,耗时(ms):" + (System.currentTimeMillis() - start));
}
@Test
public void testApacheBeanUtilsCopier() throws Exception {
long start = System.currentTimeMillis();
for (int i = 1; i <= beanCount; i++) {
UserEntity entity = createUser(i);
UserResultDto dto = new UserResultDto();
org.apache.commons.beanutils.BeanUtils.copyProperties(dto, entity);
}
System.out.println("ApacheBeanUtilsCopier,耗时(ms):" + (System.currentTimeMillis() - start));
}
@Test
public void testApachePropertyUtilsCopier() throws Exception {
long start = System.currentTimeMillis();
for (int i = 1; i <= beanCount; i++) {
UserEntity entity = createUser(i);
UserResultDto dto = new UserResultDto();
org.apache.commons.beanutils.PropertyUtils.copyProperties(dto, entity);
}
System.out.println("ApachePropertyUtilsCopier,耗时(ms):" + (System.currentTimeMillis() - start));
}
@Test
public void testSpringBeanUtilsCopier() throws Exception {
long start = System.currentTimeMillis();
for (int i = 1; i <= beanCount; i++) {
UserEntity entity = createUser(i);
UserResultDto dto = new UserResultDto();
org.springframework.beans.BeanUtils.copyProperties(entity, dto);
}
System.out.println("SpringBeanUtilsCopier,耗时(ms):" + (System.currentTimeMillis() - start));
}
private UserEntity createUser(Integer id) {
UserEntity entity = new UserEntity();
entity.setId(id);
entity.setUserName("un" + id);
entity.setRealName("rn" + id);
entity.setPassword("pwd" + id);
entity.setEmail("xyz" + id + "@126.com");
entity.setOptId(9999);
entity.setAddTime(new Date());
entity.setUpdateTime(new Date());
return entity;
}
}
源对象代码
import java.util.Date;
public class UserEntity {
private Integer id;
private String userName;
private String realName;
private String password;
private String email;
private Integer optId;
private Date addTime;
private Date updateTime;
// setter and getter methods...
}
目标对象代码
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
public class UserResultDto {
private Integer id;
private String userName;
private String realName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date addTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
// setter and getter methods...
@Override
public String toString() {
return "UserResultDto [id=" + id + ", userName=" + userName + ", realName=" + realName + ", addTime=" + addTime
+ ", updateTime=" + updateTime + "]";
}
}
测试结果
复制对象数量(beanCount) | Cglib-BeanCopier.copy耗时(ms) | Spring-BeanUtils.copyProperties耗时(ms) | Apache-BeanUtils.copyProperties耗时(ms) | Apache-PropertyUtils.copyProperties耗时(ms) |
---|---|---|---|---|
10 | 63 | 90 | 48 | 5 |
20 | 60 | 92 | 48 | 9 |
50 | 63 | 102 | 64 | 18 |
100 | 57 | 93 | 82 | 32 |
200 | 61 | 94 | 123 | 41 |
500 | 57 | 103 | 192 | 73 |
1000 | 59 | 113 | 273 | 85 |
2000 | 60 | 132 | 382 | 137 |
5000 | 61 | 164 | 603 | 289 |
10000 | 62 | 172 | 882 | 533 |
20000 | 60 | 182 | 1433 | 1011 |
50000 | 68 | 198 | 3124 | 2523 |
100000 | 74 | 234 | 5734 | 4940 |
200000 | 95 | 284 | 11105 | 10195 |
500000 | 136 | 482 | 27345 | 24385 |
总结
Cglib-BeanCopier.copy:
性能稳定高效,大量复制同一类型对象时首选,也可以通过缓存beanCopier对象来提升性能。
Spring-BeanUtils.copyProperties:
性能好,适用场景:只需要复制源对象部分属性且不需要类型转换。
Apache-BeanUtils.copyProperties:
提供了很多类型转换器,在复制过程中若类型不相同,可以自动进行类型转换,正因为如此其性能比较低。
Apache-PropertyUtils.copyProperties:
只进行简单的对象复制,不做类型转换,这也是其性能比Apache-BeanUtils.copyProperties好的主要原因。