欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-02 09:49:31
...

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的影响
            
    
    博客分类: Java javacommons-beanutilsbeanutilsPropertyDescriptorIndexedPropertyDescriptor 
  下面的这段代码展示了调用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

  • JDK1.8中IndexedPropertyDescriptor的改变对BeanUtils的影响
            
    
    博客分类: Java javacommons-beanutilsbeanutilsPropertyDescriptorIndexedPropertyDescriptor 
  • 大小: 2.8 KB