JDK1.8中IndexedPropertyDescriptor的改变对BeanUtils的影响

1. BeanUtils的应用

  调用BeanUtils.populate(object, map)可以将一个Map的按照对应的名值对转载到一个Bean对象中。这里有一个高级一点的用法。代码结构为,Father和Child分别继承自Person,Child具有Grade域而Father有Job和Children域,其中Children为一个数组类型的域。

  • Person
import java.util.Date;

public class Person implements java.io.Serializable, Cloneable{
 
    public Person() {
        super();
    }
    private String name;
    private String age;
    private Date birthday;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
 
    public String getAge() {
        return age;
    }
    public void setAge(String age) {
        this.age = age;
    }
    public Date getBirthday() {
        return birthday;
    }
    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
}


  •  Father
import java.util.ArrayList;
import java.util.List;

public class Father extends Person {
	
	private List<Child> children = new ArrayList<Child>();
	
	private String job;

	public String getJob() {
		return job;
	}
	
	public void setJob(String job) {
		this.job = job;
	}	
	
	public Child getChildren(int index){
		if (this.children.size() <= index){
			this.children.add(new Child());
		}
		return this.children.get(index);
	}
	
	public Person[] getChildren(){
		return (Person[]) children.toArray();
	}
	
	public void setChildren(int index, Child  child) {
        this.children.add(child);
    }

	
}
  •  Child
public class Child extends Person {
	
	private String grade;

	public String getGrade() {
		return grade;
	}

	public void setGrade(String grade) {
		this.grade = grade;
	}
}
  • 类图      

    JDK1.8中IndexedPropertyDescriptor的改变对BeanUtils的影响_第1张图片
  下面的这段代码展示了调用BeanUtils.populate使用一个Map填充一个Father对象。比较特别的,在Map的键值中我们使用了children[0].name这样的字符串来说明需要填充Father的children域,它是一个Child数组。其中中括号里面的0表示数组的索引。

  • BeanUtilTest
import java.lang.reflect.InvocationTargetException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.locale.converters.DateLocaleConverter;

public class BeanUtilTest {
	
	public void testPopulate() throws IllegalAccessException, InvocationTargetException
	{
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("name", "tan");
		map.put("birthday", "1980-6-1");
		map.put("children[0].name", "zihui");
		map.put("children[0].birthday", "2008-2-13");
		map.put("children[0].grade", "G3");
		map.put("job", "engineer");		
		
		ConvertUtils.register(new DateLocaleConverter(), Date.class);
		Father f = new Father();
		
		BeanUtils.populate(f, map);
		System.out.println(f.getName());
		System.out.println(f.getJob());
		System.out.println(f.getChildren(0).getName());
		System.out.println(f.getChildren(0).getGrade());
	}
	
	public static void main(String[] args) throws IllegalAccessException, InvocationTargetException {
		
		BeanUtilTest but = new BeanUtilTest();
		but.testPopulate();
	}
}
  •  执行结果

  此代码在JDK1.7.0_60的环境中执行结果如下:

tan
engineer
zihui
G3

 

2. 升级JDK1.8.0_102之后

把jre library升级成JDK1.8.0_102执行此代码出错。错误信息如下:

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.commons.beanutils.PropertyUtilsBean.invokeMethod(PropertyUtilsBean.java:2116)
	at org.apache.commons.beanutils.PropertyUtilsBean.getIndexedProperty(PropertyUtilsBean.java:542)
	at org.apache.commons.beanutils.PropertyUtilsBean.getIndexedProperty(PropertyUtilsBean.java:446)
	at org.apache.commons.beanutils.PropertyUtilsBean.getNestedProperty(PropertyUtilsBean.java:806)
	at org.apache.commons.beanutils.PropertyUtilsBean.getProperty(PropertyUtilsBean.java:884)
	at org.apache.commons.beanutils.BeanUtilsBean.setProperty(BeanUtilsBean.java:894)
	at org.apache.commons.beanutils.BeanUtilsBean.populate(BeanUtilsBean.java:821)
	at org.apache.commons.beanutils.BeanUtils.populate(BeanUtils.java:431)
	at BeanUtilTest.testPopulate(BeanUtilTest.java:27)
	at BeanUtilTest.main(BeanUtilTest.java:37)
Caused by: java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [LPerson;
	at Father.getChildren(Father.java:27)
	... 14 more

 

3. 寻找错误原因

通过调试jdk 1.7和jdk 1.8,发现直接原因是jdk1.7下PropertyUtilsBean.getIndexedProperty(Object bean,String name, int index)在521行返回,而jdk1.8在542行抛出异常。

  • 代码片段17行为源代码521行,38行为源代码542行
        PropertyDescriptor descriptor =
                getPropertyDescriptor(bean, name);
        if (descriptor == null) {
            throw new NoSuchMethodException("Unknown property '" +
                    name + "' on bean class '" + bean.getClass() + "'");
        }

        // Call the indexed getter method if there is one
        if (descriptor instanceof IndexedPropertyDescriptor) {
            Method readMethod = ((IndexedPropertyDescriptor) descriptor).
                    getIndexedReadMethod();
            readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
            if (readMethod != null) {
                Object[] subscript = new Object[1];
                subscript[0] = new Integer(index);
                try {
                    return (invokeMethod(readMethod,bean, subscript));
                } catch (InvocationTargetException e) {
                    if (e.getTargetException() instanceof
                            IndexOutOfBoundsException) {
                        throw (IndexOutOfBoundsException)
                                e.getTargetException();
                    } else {
                        throw e;
                    }
                }
            }
        }

        // Otherwise, the underlying property must be an array
        Method readMethod = getReadMethod(bean.getClass(), descriptor);
        if (readMethod == null) {
            throw new NoSuchMethodException("Property '" + name + "' has no " +
                    "getter method on bean class '" + bean.getClass() + "'");
        }

        // Call the property getter and return the value
        Object value = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);

   进一步阅读代码,我们可以判断出jdk1.7和jdk1.8对person的children property返回的PropertyDescriptor不同,导致了这段代码出现了异常。jdk1.7返回的是IndexedPropertyDescriptor,而jdk1.8返回的则不是IndexedPropertyDescriptor。

 

4. 验证错误原因

  简化测试代码如下

  • PropertyDescriptorTest
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.List;

public class PropertyDescriptorTest {

	public static void main(String[] args) throws IntrospectionException {
		BeanInfo info2 = Introspector.getBeanInfo(Father.class);
        PropertyDescriptor[] descriptors2 = info2.getPropertyDescriptors();
        for (int i = 0; i < descriptors2.length; i++) {
            System.out.println(descriptors2[i].getClass().getName() + ":" + descriptors2[i].getName());
        }

	}

}

 

  • jdk1.7的测试结果

 

java.beans.PropertyDescriptor:age
java.beans.PropertyDescriptor:birthday
java.beans.IndexedPropertyDescriptor:children
java.beans.PropertyDescriptor:class
java.beans.PropertyDescriptor:job
java.beans.PropertyDescriptor:name

 

  • jdk1.8的测试结果

 

java.beans.PropertyDescriptor:age
java.beans.PropertyDescriptor:birthday
java.beans.PropertyDescriptor:children
java.beans.PropertyDescriptor:class
java.beans.PropertyDescriptor:job
java.beans.PropertyDescriptor:name

   以上测试结果证明了我们的猜测。

 

5.比较jdk1.7和jdk1.8源代码,找出根本原因

  java.beans.Introspector类通过getBeanInfo产生了一个BeanInfo来描叙一个java bean,BeanInfo中包含每个域的描叙PropertyDescriptor,由getPropertyDescriptors返回一个PropertyDescriptor数组。

  而在初始化BeanInfo的方法Introspector.getBeanInfo(Father.class)中,通过调试,可以看出 PropertyDescriptor是在Introspector的私有方法processPropertyDescriptors中被初始化的。比较jdk1.7和jdk1.8的源代码,可以看出这个私有方法有很大的变动。

  进一步调试,我发现影响children的PropertyDescriptor类型被判断成PropertyDescriptor的关键代码是jdk1.8 类Introspector的748到764行的逻辑。代码如下:

                if (pd == null) {
                    pd = ipd;
                } else {
                    Class<?> propType = pd.getPropertyType();
                    Class<?> ipropType = ipd.getIndexedPropertyType();
                    if (propType.isArray() && propType.getComponentType() == ipropType) {
                        pd = pd.getClass0().isAssignableFrom(ipd.getClass0())
                                ? new IndexedPropertyDescriptor(pd, ipd)
                                : new IndexedPropertyDescriptor(ipd, pd);
                    } else if (pd.getClass0().isAssignableFrom(ipd.getClass0())) {
                        pd = pd.getClass0().isAssignableFrom(ipd.getClass0())
                                ? new PropertyDescriptor(pd, ipd)
                                : new PropertyDescriptor(ipd, pd);
                    } else {
                        pd = ipd;
                    }
                }

  反观jdk1.7的代码,我们可以看出此逻辑为jdk1.8独有的逻辑,初步判读jdk1.8针对IndexedPropertyDescriptor的判断有了一些新的特征。通过调试,发现因为没有满足以下条件,所以children属性被判断成普通的PropertyDescriptor而非我们期望的IndexedPropertyDescriptor。

 

if (propType.isArray() && propType.getComponentType() == ipropType) { 

   其中propType.isArray()返回为真,因此我们判断出getChildren方法的返回类型必须一致才能够满足条件。

 

6.修改

修改Father类的定义。

  • new Father代码如下: 
import java.util.ArrayList;
import java.util.List;

public class Father extends Person {
	
	private List<Child> children = new ArrayList<Child>();
	
	private String job;

	public String getJob() {
		return job;
	}
	
	public void setJob(String job) {
		this.job = job;
	}
	
	
	public Child getChildren(int index){
		if (this.children.size() <= index){
			this.children.add(new Child());
		}
		return this.children.get(index);
	}
	
	//Fix return type, keep it consistance with getChildren(int index)
	public Child[] getChildren(){
		return (Child[]) children.toArray();
	}
	
	public void setChildren(int index, Child  child) {
        this.children.add(child);
    }	
}

   在jkd1.8上执行测试方法,结果符合我们的期望:

 

java.beans.PropertyDescriptor:age
java.beans.PropertyDescriptor:birthday
java.beans.IndexedPropertyDescriptor:children
java.beans.PropertyDescriptor:class
java.beans.PropertyDescriptor:job
java.beans.PropertyDescriptor:name
  验证主程序,主程序成功执行,没有异常抛出: 
tan
engineer
zihui
G3
  

7.思考

  java.beans在jdk1.8中针对PropertyDescriptor的一些调整导致common-beanutils出现该错误。而common-beanutils在现如今的项目中使用非常普遍,所以当建议项目在从jdk1.7升级到jdk1.8的过程中,要有针对性的组织和该代码相关的测试案例,从而避免交付结果中存在潜在的问题。

 

8.相关资料

BeanUtils: http://commons.apache.org/proper/commons-beanutils/

BeanUtils: commons-beanutils-1.9.2

JDK1.7: JDK1.7.0_60

JDK1.8: JDK1.8.0_102

你可能感兴趣的:(java,BeanUtils)