记录org.springframework.beans.BeanUtils的copyProperties方法

2,483 阅读5分钟

今天开发的时候在调用BeanUtils.copyProperties()的时候出现了异常,在网上找了一些这个方法的介绍,发现与我测试的结果不同,便记录一下我的测试过程与结果。

一.简介

首先简单介绍一下我用到这个方法的场景,有一个JavaBean的属性大部分都在另一个JavaBean中存在,现在需要把这个JavaBean中的同名的属性的值赋给另一个JavaBean对象,如果用get/set处理,需要大量的代码,用这个工具类的方法就能起到简化代码的作用。

这个方法有三个public修饰的重载方法,但其实都是调用的同一个方法copyProperties(Object source, Object target, Class editable, String[] ignoreProperties)

    public static void copyProperties(Object source, Object target)
        throws BeansException
    {
        copyProperties(source, target, null, null);
    }
    public static void copyProperties(Object source, Object target, Class editable)
        throws BeansException
    {
        copyProperties(source, target, null, null);
    }
   public static void copyProperties(Object source, Object target, String[] ignoreProperties)
       throws BeansException
   {
       copyProperties(source, target, null, null);
   }

二.原理

下面是此方法的源码

    public 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 ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;

        for (int i = 0; i < targetPds.length; i++) {
            PropertyDescriptor targetPd = targetPds[i];
            if ((targetPd.getWriteMethod() != null) && ((ignoreProperties == null) || (!ignoreList.contains(targetPd.getName()))))
            {
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                if ((sourcePd != null) && (sourcePd.getReadMethod() != null)) {
                    try {
                        Method readMethod = sourcePd.getReadMethod();
                        if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                            readMethod.setAccessible(true);
                        }
                        Object value = readMethod.invoke(source, new Object[0]);
                        Method writeMethod = targetPd.getWriteMethod();
                        if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                            writeMethod.setAccessible(true);
                        }
                        writeMethod.invoke(target, new Object[] { value });
                    }
                    catch (Throwable ex) {
                        throw new FatalBeanException("Could not copy properties from source to target", ex);
                    }
                }
            }
        }
    }

注意这两处代码

    Object value = readMethod.invoke(source, new Object[0]);
    
    writeMethod.invoke(target, new Object[] { value });

从源码可以看到此方法是利用反射机制对JavaBean的属性进行处理,先调用source对象的getter方法,取出各个属性的值并赋给Object对象value,再利用反射调用target对象的setter方法将value对象的值赋给相同的属性。

三.需要注意的地方

  1. 如果source对象的某个对象的属性值为null,而target对象的对应属性为基本类型,就会抛IllegalArgumentException异常,这点容易理解,给基本数据类型赋null值,所以一般在JavaBean中使用包装类型。
Exception in thread "main" org.springframework.beans.FatalBeanException: Could not copy properties from source to target; nested exception is java.lang.IllegalArgumentException
	at org.springframework.beans.BeanUtils.copyProperties(BeanUtils.java:591)
	at org.springframework.beans.BeanUtils.copyProperties(BeanUtils.java:500)
	at com.haiyi.action.ElectronNote.Test.main(Test.java:21)
Caused by: java.lang.IllegalArgumentException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at org.springframework.beans.BeanUtils.copyProperties(BeanUtils.java:588)
	... 2 more
  1. 从上面可以看到,该方法对属性的复制属于浅克隆,所以若是有一个list对象,赋值之后对source对象中的list进行修改,target对象中的属性也会随之修改。

  2. 我在网上看到有人说不支持java.util.Date,所以特地测试了一下,但是并没有报错,所以这个说法有待考究,应该不是这个方法的原因。

    下面是我的测试代码(getter和setter方法就不展示了)

import java.util.Date;
import java.util.List;

public class Dog {

	private Integer id;
	
	private String name;
	
	private String bark;
	
	private Date birthday;
	
	private List<String> children;
}

public class Cat {

	private int id;
	
	private String name;
	
	private String meow;
	
	private Date birthday;
	
	private List<String> children;

	@Override
	public String toString() {
		return "Cat [id=" + id + ", name=" + name + ", meow=" + meow
				+ ", birthday=" + birthday + ", children=" + children + "]";
	}
}

import java.util.ArrayList;
import java.util.Date;

import org.springframework.beans.BeanUtils;

public class Test {
	
	public static void main(String[] args) {
		Dog d = new Dog();
		d.setId(1); //如果屏蔽掉这行代码,会报错
		d.setName("旺财");
		d.setBark("汪");
		d.setBirthday(new Date());
		
		ArrayList<String> children = new ArrayList<String>();
		children.add("小黑");
		children.add("小白");
		d.setChildren(children);
		
		Cat c = new Cat();
		BeanUtils.copyProperties(d, c);
		//如果需要重新拷贝一份可以使用下面代码
		//c.setChildren(new ArrayList<String>(d.getChildren()));
		System.out.println(c);
		children.add("小花");
		System.out.println(c);
	}
}

下面是测试结果

Cat [id=1, name=旺财, meow=null, birthday=Wed Feb 27 10:15:29 CST 2019, children=[小黑, 小白]]
Cat [id=1, name=旺财, meow=null, birthday=Wed Feb 27 10:15:29 CST 2019, children=[小黑, 小白, 小花]]

如果不将注释的代码屏蔽,结果会是

Cat [id=1, name=旺财, meow=null, birthday=Wed Feb 27 16:19:07 CST 2019, children=[小黑, 小白]]
Cat [id=1, name=旺财, meow=null, birthday=Wed Feb 27 16:19:07 CST 2019, children=[小黑, 小白]]

看一下ArrayList 的构造方法

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        size = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

可以看到是调用了toArray()方法,这个方法是在Collection接口中定义的,来看看ArrayList是怎么实现这个方法的

/**
     * Returns an array containing all of the elements in this list
     * in proper sequence (from first to last element).
     *
     * <p>The returned array will be "safe" in that no references to it are
     * maintained by this list.  (In other words, this method must allocate
     * a new array).  The caller is thus free to modify the returned array.
     *
     * <p>This method acts as bridge between array-based and collection-based
     * APIs.
     *
     * @return an array containing all of the elements in this list in
     *         proper sequence
     */
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

从这个方法的描述的第二段可以看到,这个方法将返回一个新的数组,所以调用者可以随意的修改返回的数组。所以第二次测试数组中的值并没有被改变,从Arrays的源码中也能看到new了一个新的Object数组。

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

四.总结

如果JavaBean中含有大量的属性,这个方法将极大的简化我们的代码,而且只会把相同的属性的值复制,如果之前已经有值了,将覆盖之前的值。

这是我第一次写点东西,将自己平时开发中遇到的问题记录起来,希望将来能养成这个良好的习惯吧。如果发现哪里有误,可以给我留言,我及时更正。