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