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; } }
- 类图
下面的这段代码展示了调用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
上一篇: 遍历集合删除元素