Osmanthus

空想具現化


  • 首页
  • 归档
  • 分类
  • 标签
  • 关于
  •   

© 2024 Homurax

UV: | PV:

Theme Typography by Makito

Proudly published with Hexo

JavaBean 中的 Field、Property 和 Introspector

发布于 2020-03-17 坑  JavaBean Introspector Lombok 

踩了一个其实是知道的坑,记录一下,不要犯同样的错误。

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 符合规范就是最好的处理方式。我这里遇到的则是历史遗留问题,应该是迁移时为了尽可能减少造成影响,而没有规范属性名。

 上一篇: Java 类型系统 下一篇: Jetbrains Quest 2 记录 

© 2024 Homurax

UV: | PV:

Theme Typography by Makito

Proudly published with Hexo