BeanUtils.copyProperties使用和性能分析

BeanUtils.copyProperties使用和性能分析介绍BeanUtils.copyProperties的使用,对比Apache CommonBeanUtils和Spring BeanUtils,进行性能分析

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应该都属于POJO POJO是最多变的对象,是一个中间对象。一个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);
}
类别 所在的包 函数参数 性能
Apache org.apache.commons.beanutils.BeanUtils copyProperties(Object dest, Object orig) 较差
Spring org.springframework.beans copyProperties(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 倍的性能差异!

实现 100 1000 10000 100000 1000000
StaticCglibBeanCopier 0.053561 0.680016 1.196787 2.924973 10.769302
CglibBeanCopier 4.099259 12.252336 33.50937 48.940261 104.005539
SpringBeanUtils 3.80229 9.268228 26.635362 118.699586 162.996875
CommonPropertyUtils 6.7971116 20.59255 49.380707 219.271803 1857.382452
CommonBeanUtils 23.566713 106.971358 473.95897 2619.76277 26199.132175

今天的文章BeanUtils.copyProperties使用和性能分析分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/18167.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注