BeanUtils.copyProperties使用和性能分析

10,967 阅读10分钟

Ref

Java对象——PO,VO,DAO,BO,POJO

对象释意使用备注
PO(persistant object)持久对象可以看成是与数据库中的表相映射的Java对象,最简单的PO就是对应数据库中某个表中的一条记录。PO中应该不包含任何对数据库的操作
VO(view object)表现层对象主要对应界面显示的数据对象。对于一个WEB页面,或者SWT、SWING的一个界面,用一个VO对象对应整个界面的值。
BO(business object)业务对象封装业务逻辑的 Java 对象通过调用DAO方法,结合PO,VO进行业务操作。
POJO(plain ordinary java object)简单无规则Java对象POJO和其他对象不是一个层面上的对象划分,VO和PO应该都属于POJOPOJO是最多变的对象,是一个中间对象。一个POJO持久化以后就是PO,直接用它传递、传递过程中就是DTO,直接用来对应表示层就是VO
DAO(data access object)数据访问对象此对象用于访问数据库。通常和PO结合使用,DAO中包含了各种数据库的操作方法。通过它的方法,结合PO对数据库进行相关的操作。
DTO (data transfer object)数据传输对象主要用于远程调用等需要大量传输对象的地方。比如我们一张表有100个字段,那么对应的PO就有100个属性。但是界面上只要显示10个字段,客户端用WEB service 来获取数据,没有必要把整个PO对象传递到客户端。这时就可以用只有这 10 个属性的 DTO 来传递结果到客户端,这样也不会暴露服务端表结构。到达客户端以后,如果用这个对象来对应界面显示,那此时它的身份就转为VO

Overview

在开发中,常使用 BeanUtils.copyProperties() 进行PO,VO,DTO等对象的复制和转换。

BeanUtils 提供对 Java 反射和自省 API 的包装。其主要目的是利用反射机制对 Java Bean 的属性进行处理,在这里只介绍它的 copyProperties() 方法。

需要注意的是

  1. 使用 Spring 的 BeanUtils # CopyProperties 方法去拷贝对象属性时,需要对应的属性有 gettersetter 方法(内部实现时,使用反射拿到 setget 方法,再去获取/设置属性值);
  2. 如果存在属性完全相同得内部类,但不是同一个内部类(即分别属于各自得内部类),则 Spring 会认为属性不同,不会Copy;
  3. 泛型只在编译期起作用,不能依靠泛型来做运行期的限制;
  4. Spring 和 Apache 的 copy 属性得方法源和目的参数得位置正好相反,所以导包和调用得时候需要注意。

Method

// Apache 将源对象中的值拷贝到目标对象 
// 注意,目标对象在前
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
    BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

如果你有两个具有很多相同属性的 Java Bean,一个很常见的情况就是 Struts 里的 PO 对象(持久对象)和对应的 ActionForm。例如 Teacher 和 TeacherForm。我们一般会在 Action 里从 ActionForm 构造一个 PO 对象,传统的方式是使用类似下面的语句对属性逐个赋值。

//得到TeacherForm   
TeacherForm teacherForm=(TeacherForm)form; 
//构造Teacher对象   
Teacher teacher=new Teacher(); 
//赋值   
teacher.setName(teacherForm.getName()); 
teacher.setAge(teacherForm.getAge()); 
teacher.setGender(teacherForm.getGender()); 
teacher.setMajor(teacherForm.getMajor()); 
teacher.setDepartment(teacherForm.getDepartment()); 
//持久化Teacher对象到数据库   
HibernateDAO.save(teacher);

而使用 BeanUtils 后,代码就大大改观了,如下所示

//得到TeacherForm   
TeacherForm teacherForm=(TeacherForm)form;   
  
//构造Teacher对象   
Teacher teacher = new Teacher();   
  
//赋值   
BeanUtils.copyProperties(teacher,teacherForm);   
  
//持久化Teacher对象到数据库   
HibernateDAO.save(teacher);  

如果 Teacher 和 TeacherForm 间存在名称不相同的属性,则 BeanUtils 不对这些属性进行处理,需要程序员手动处理。例如 Teacher 包含 modifyDate(该属性记录最后修改日期,不需要用户在界面中输入)属性,而 TeacherForm 无此属性,那么在上面代码的 copyProperties() 后还要加上一句

teacher.setModifyDate(new Date()); 

除 BeanUtils 外,还有一个名为 PropertyUtils 的工具类,它也提供 copyProperties() 方法,作用与 BeanUtils 的同名方法十分相似,主要的区别在于后者提供类型转换功能,即发现两个 Java Bean的同名属性为不同类型时,在支持的数据类型范围内进行转换,而前者不支持这个功能,但是速度会更快一些。BeanUtils 支持的转换类型如下

  • java.lang.BigDecimal
  • java.lang.BigInteger
  • boolean and java.lang.Boolean
  • byte and java.lang.Byte
  • char and java.lang.Character
  • java.lang.Class
  • double and java.lang.Double
  • float and java.lang.Float
  • int and java.lang.Integer
  • long and java.lang.Long
  • short and java.lang.Short
  • java.lang.String
  • java.sql.Date
  • java.sql.Time
  • java.sql.Timestamp

这里要注意一点,java.util.Date 是不被支持的,而它的子类 java.sql.Date 是被支持的。因此如果对象包含时间类型的属性,且希望被转换的时候,一定要使用 java.sql.Date 类型。否则在转换时会提示 argument mistype 异常。

优缺点

// Apache 将源对象中的值拷贝到目标对象 
// 注意,目标对象在前
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
    BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

BeanUtils.copyProperties(destObject, origObjecr)  

上述方法的优点是可以简化代码。

缺点是使用 BeanUtils 的成本惊人地昂贵!BeanUtils 所花费的时间要超过取数据、将其复制到对应的 destObject 对象(通过手动调用 get 和 set 方法),以及通过串行化将其返回到远程的客户机的时间总和。所以要小心使用这种威力!

采坑

在使用时,需要注意如下几点

  1. 使用 Spring 的 BeanUtils # CopyProperties 方法去拷贝对象属性时,需要对应的属性有 gettersetter 方法(内部实现时,使用反射拿到 setget 方法,再去获取/设置属性值);
  2. 如果存在属性完全相同得内部类,但不是同一个内部类(即分别属于各自得内部类),则 Spring 会认为属性不同,不会Copy;
  3. 泛型只在编译期起作用,不能依靠泛型来做运行期的限制;
  4. Spring 和 Apache 的 copy 属性得方法源和目的参数得位置正好相反,所以导包和调用得时候需要注意。

内部类的拷贝

如果存在属性完全相同得内部类,但不是同一个内部类(即分别属于各自得内部类),则 Spring 会认为属性不同,不会Copy。

下面给出例子进行验证。

@Data
public class TestEntity{
    private Integer age;
    private String name;
    private Inner inner;
    
    @Data
    public static class Inner{
        private Integer a;
        public Inner(Integer a){
            this.a = a;
        }
    }
    
}
@Data
public class TestVO{
    private Integer age;
    private String name;
    private Inner inner;
    

    @Data
    public static class Inner{
        private Integer a;
        public Inner(Integer a){
            this.a = a;
        }
    }

}
public class Main{
    public static void main(String args[]){
        TestEntity entity = new TestEntity();
        entity.setAge(1);
        entity.setName("hehe");
        entity.setInner(new TestEntity.Inner(1));

        TestVO vo = new TestVO();
        BeanUtils.copyProperties(entity,vo);
        System.out.println(vo.toString());
        
    }   
}

执行代码,程序输入如下。可以看到,对象的 inner 是空的。

TestVO(age=1, name=lbs, inner=null)

查看编译后的 .class 文件。可以看到,TestEntity.javaTestVO.java 里面的 Inner 在编译之后的 class 名字不一样(代表加载到虚拟机之后的地址不同),因此无法拷贝成功。

// 编译后的 .class 文件
TestEntity$Inner.class
TestEntity.class

TestVO$Inner.class
TestVO.class

那么问题来了哈,我们怎样用才能让其拷贝成功呢?将代码修改如下。

@Data
public class TestVO{
    private Integer age;
    private String name;
    private TestEntity.Inner inner;
}

仅仅是把 Inner 变为了 TestEntity.Inner,删掉了没引用得内部类 Inner,再次执行测试代码,然后运行结果如下

TestVO(age=1, name=lbs, inner=TestEntity.Inner(a=1))

可以看到, TestEntity 对象里面的 inner 被成功拷贝过来。此时编译后的 class 文件也由4个变为了3个。

// 编译后的 .class 文件
TestEntity$Inner.class
TestEntity.class
TestVO.class

Spring 和 Apache 的 copyProperties

Apache中,org.apache.commons.beanutils.BeanUtils # copyProperties 源码如下。

// Apache 将源对象中的值拷贝到目标对象 
// 注意,目标对象在前
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
    BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

默认情况下,使用 org.apache.commons.beanutils.BeanUtils 对复杂对象的复制是引用,这是一种浅拷贝

由于 Apache 下的 BeanUtils 对象拷贝性能太差,不建议使用,而且在阿里巴巴 Java 开发规约插件上也明确指出

Ali-Check | 避免用Apache Beanutils进行属性的copy。

commons-beantutils 对于对象拷贝加了很多的检验,包括类型的转换,甚至还会检验对象所属的类的可访问性,可谓相当复杂,这也造就了它的差劲的性能,具体实现代码如下。

    public void copyProperties(final Object dest, final Object orig)
        throws IllegalAccessException, InvocationTargetException {

        // Validate existence of the specified beans
        if (dest == null) {
            throw new IllegalArgumentException
                    ("No destination bean specified");
        }
        if (orig == null) {
            throw new IllegalArgumentException("No origin bean specified");
        }
        if (log.isDebugEnabled()) {
            log.debug("BeanUtils.copyProperties(" + dest + ", " +
                      orig + ")");
        }

        // Copy the properties, converting as necessary
        if (orig instanceof DynaBean) {
            final DynaProperty[] origDescriptors =
                ((DynaBean) orig).getDynaClass().getDynaProperties();
            for (DynaProperty origDescriptor : origDescriptors) {
                final String name = origDescriptor.getName();
                // Need to check isReadable() for WrapDynaBean
                // (see Jira issue# BEANUTILS-61)
                if (getPropertyUtils().isReadable(orig, name) &&
                    getPropertyUtils().isWriteable(dest, name)) {
                    final Object value = ((DynaBean) orig).get(name);
                    copyProperty(dest, name, value);
                }
            }
        } else if (orig instanceof Map) {
            @SuppressWarnings("unchecked")
            final
            // Map properties are always of type <String, Object>
            Map<String, Object> propMap = (Map<String, Object>) orig;
            for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
                final String name = entry.getKey();
                if (getPropertyUtils().isWriteable(dest, name)) {
                    copyProperty(dest, name, entry.getValue());
                }
            }
        } else /* if (orig is a standard JavaBean) */ {
            final PropertyDescriptor[] origDescriptors =
                getPropertyUtils().getPropertyDescriptors(orig);
            for (PropertyDescriptor origDescriptor : origDescriptors) {
                final String name = origDescriptor.getName();
                if ("class".equals(name)) {
                    continue; // No point in trying to set an object's class
                }
                if (getPropertyUtils().isReadable(orig, name) &&
                    getPropertyUtils().isWriteable(dest, name)) {
                    try {
                        final Object value =
                            getPropertyUtils().getSimpleProperty(orig, name);
                        copyProperty(dest, name, value);
                    } catch (final NoSuchMethodException e) {
                        // Should not happen
                    }
                }
            }
        }

    }

而 Spring 的 copyProperties 的使用如下。

package org.springframework.beans;
// Spring 将源对象中的值拷贝到目标对象 
// 注意,目标对象在后。
public static void copyProperties(Object source, Object target) throws BeansException {
    copyProperties(source, target, null, (String[]) null);
}

对比 Spring 和 Apache 的 copyProperties 方法,可以发现两者参数顺序不一样,在使用时一定要注意这个区别。

package org.apache.commons.beanutils.BeanUtils;
// Apache 将源对象中的值拷贝到目标对象 
// 注意,目标对象在前
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
    BeanUtilsBean.getInstance().copyProperties(dest, orig);
}
类别所在的包函数参数性能
Apacheorg.apache.commons.beanutils.BeanUtilscopyProperties(Object dest, Object orig)较差
Springorg.springframework.beanscopyProperties(Object source, Object target)较好

Spring 下的 BeanUtils # copyProperties 方法实现比较简单,就是对两个对象中相同名字的属性进行简单的 get/set,仅检查属性的可访问性,因此具有较好的性能,优于 Apache 的 copyProperties。具体实现如下。

    //Spring  中  copyProperties
    private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
			@Nullable 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);
						}
					}
				}
			}
		}
	}

可以看到,成员变量赋值是基于目标对象的成员列表,但必须保证同名的两个成员变量类型相同。

包装类型

在进行属性拷贝时,低版本的 Apache CommonsBeanUtils 为了解决 Date 为空的问题,会导致为目标对象的原始类型的包装类属性赋予初始值。如 Integer 属性默认赋值为 0,尽管你的来源对象该字段的值为 null。

这个在我们的包装类属性为 null 值时有特殊含义的场景,非常容易踩坑!例如搜索条件对象,一般 null 值表示该字段不做限制,而 0 表示该字段的值必须为0。

改用其他工具时

如上一章节 「Spring 和 Apache 的 copyProperties」所讲,知道了 Apache CommonsBeanUtils 的性能较差后,要改用 Spring 的 BeanUtils 时,需要注意两者的参数顺序问题。记得将 targetObjectsourceObject 两个参数顺序对调。

BeanUtils性能测试

参见上述参考链接1的测试结果,CglibBeanCopier 的拷贝速度是最快的,即使是百万次的拷贝也只需要 10 毫秒! 相比而言,最差的是 Apache Commons 包的 BeanUtils.copyProperties 方法,100 次拷贝测试与表现最好的 Cglib 相差 400 倍之多。百万次拷贝更是出现了 2600 倍的性能差异!

实现1001000100001000001000000
StaticCglibBeanCopier0.0535610.6800161.1967872.92497310.769302
CglibBeanCopier4.09925912.25233633.5093748.940261104.005539
SpringBeanUtils3.802299.26822826.635362118.699586162.996875
CommonPropertyUtils6.797111620.5925549.380707219.2718031857.382452
CommonBeanUtils23.566713106.971358473.958972619.7627726199.132175