JavaBean 中的 Field、Property 和 Introspector
踩了一个其实是知道的坑,记录一下,不要犯同样的错误。
java.lang.NoSuchMethodException: Unknown property '' on class ''
at org.apache.commons.beanutils.PropertyUtilsBean.getSimpleProperty(PropertyUtilsBean.java:1270)
at org.apache.commons.beanutils.PropertyUtilsBean.getNestedProperty(PropertyUtilsBean.java:809)
at org.apache.commons.beanutils.PropertyUtilsBean.getProperty(PropertyUtilsBean.java:885)
at org.apache.commons.beanutils.PropertyUtils.getProperty(PropertyUtils.java:464)
这个异常是业务上在使用 PropertyUtils.getProperty(final Object bean, final String name)
去获取属性值时发生的。
异常信息简单来看的话就是 class 中没有某个 property,但是实际是有的。简化为以下场景复现。
import lombok.Data;
import org.apache.commons.beanutils.PropertyUtils;
import java.lang.reflect.InvocationTargetException;
@Data
public class AObject {
private String aId;
private String normalId;
public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
final AObject aObject = new AObject();
final Object aId = PropertyUtils.getProperty(aObject, "aId");
System.out.println(aId);
}
}
通过打断点追踪源码,可以发现异常是在 PropertyUtilsBean.java
中的 getSimpleProperty()
方法里抛出的。
public Object getSimpleProperty(final Object bean, final String name)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
// 略过
// Retrieve the property getter method for the specified property
final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
if (descriptor == null) {
throw new NoSuchMethodException("Unknown property '" +
name + "' on class '" + bean.getClass() + "'" );
}
final Method readMethod = getReadMethod(bean.getClass(), descriptor);
if (readMethod == null) {
throw new NoSuchMethodException("Property '" + name +
"' has no getter method in class '" + bean.getClass() + "'");
}
// 略过
}
进一步查看 getPropertyDescriptor()
方法。
public PropertyDescriptor getPropertyDescriptor(Object bean, String name)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
// 略过
final BeanIntrospectionData data = getIntrospectionData(bean.getClass());
PropertyDescriptor result = data.getDescriptor(name);
if (result != null) {
return result;
}
FastHashMap mappedDescriptors = getMappedPropertyDescriptors(bean);
if (mappedDescriptors == null) {
mappedDescriptors = new FastHashMap();
mappedDescriptors.setFast(true);
mappedDescriptorsCache.put(bean.getClass(), mappedDescriptors);
}
result = (PropertyDescriptor) mappedDescriptors.get(name);
if (result == null) {
// not found, try to create it
try {
result = new MappedPropertyDescriptor(name, bean.getClass());
} catch (final IntrospectionException ie) {
/* Swallow IntrospectionException
* TODO: Why?
*/
}
if (result != null) {
mappedDescriptors.put(name, result);
}
}
return result;
}
result 是空的,打断点观察 data.getDescriptor(name)
部分。
分析至此,简单来说 PropertyUtils.getProperty(final Object bean, final String name)
的工作原理就是构建了 class 中 field name 与 getter 的映射关系,然后通过参数 name 尝试获取 Method,再调用 invoke 去反射获取属性值。
对于示例来说就是通过 aId
,没有获取到 Method getAId
。因为构建的映射关系中的 key(name) 是 AId
。
顺着 final BeanIntrospectionData data = getIntrospectionData(bean.getClass());
继续看。
最后可以发现 java.beans.BeanInfo
是在 org.apache.commons.beanutils.DefaultBeanIntrospector
中获取的。
public void introspect(final IntrospectionContext icontext) {
BeanInfo beanInfo = null;
try {
beanInfo = Introspector.getBeanInfo(icontext.getTargetClass());
} catch (final IntrospectionException e) {
// no descriptors are added to the context
log.error(
"Error when inspecting class " + icontext.getTargetClass(), e);
return;
}
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
if (descriptors == null) {
descriptors = new PropertyDescriptor[0];
}
handleIndexedPropertyDescriptors(icontext.getTargetClass(),
descriptors);
icontext.addPropertyDescriptors(descriptors);
}
继续看 java.beans.Introspector
的代码可以得知,是通过 getter/setter 的方法名来获得 key。
可见 decapitalize()
方法的处理逻辑就是:如果首字母与第二个字母为大写(显然总长度要大于1),则原样返回;否则首字母变为小写后返回。
所以方法名中截取的 AId
转换后的 key 是 AId
。
方法上的注释内容出自 JavaBeans(TM) Specification 1.01 Final Release
至此,抛出的异常的原因已经很清楚了。
之所以 decapitalize()
方法要如此处理,是因为 JavaBeans 规范中对 field name 不止考虑到了常见的命名情况,也考虑到了一些有特定含义的英文大写缩写,如 URL
。
驼峰命名的起始部分(或者整个 field)可以全部是大写的。变量的「前两个字母」要么全大写,要么全小写。
此外对于 field aId
, lombok 的 Data 注解为我们生成的是 getAId()/setAId()
。
如果使用 IDE 的快捷键生成 getter/setter ,生成的是 getaId()/setaId()
,但是还是会提示你这里是一处 Typo 。
总结,一开始 field name 符合规范就是最好的处理方式。我这里遇到的则是历史遗留问题,应该是迁移时为了尽可能减少造成影响,而没有规范属性名。