前言

在调用远程接口返回的数据数据类型为Map,在这个Map中有一个sRowIndex字段,第一眼看到这个字段的时候就觉得有问题,却又说不出哪里的问题。果然在使用BeanUtils.populate(bean, map)将map映射到实体类的时候,该字段死活为null,此为前提。

在网上搜索了一会后,有了自己的想法,于是断点调试,一步一步跟随,下为记录:

记录

前提:

BeanUtils类为commons-beanutils-1.9.3下的org.apache.commons.beanutils;

JDK版本为1.8

断点记录

1. 在程序中调用 BeanUtils.populate(bean, map);实际上是调用的org.apache.commons.beanutils.BeanUtilsBean#populate方法,其中有一个setProperty方法


    public void populate(final Object bean, final Map properties)
        throws IllegalAccessException, InvocationTargetException {
        //===========省略=============

        // Loop through the property name/value pairs to be set
        for(final Map.Entry entry : properties.entrySet()) {
            // Identify the property name and value(s) to be assigned
            final String name = entry.getKey();
            if (name == null) {
                continue;
            }

            // Perform the assignment for this property
            setProperty(bean, name, entry.getValue());

        }

    }

不难想到,此方法就是为这个bean的name属性设置entry.getValue值的。

2. 步入到setProperties方法中,可以看到很长的代码一串,大概撸了一眼,重要的部分用红色标明:


 public void setProperty(final Object bean, String name, final Object value)
        throws IllegalAccessException, InvocationTargetException {

       //=======================省略====================
        
        // Calculate the property type
        if (target instanceof DynaBean) {
       //=======================省略====================
        } else if (target instanceof Map) {
            type = Object.class;
        } else if (target != null && target.getClass().isArray() && index >= 0) {
            type = Array.get(target, index).getClass();
        } else {
            PropertyDescriptor descriptor = null;
            try {
                descriptor =
                    getPropertyUtils().getPropertyDescriptor(target, name);
                if (descriptor == null) {
                    return; // Skip this property setter
                }
            } catch (final NoSuchMethodException e) {
                return; // Skip this property setter
            }
         //======================省略====================
        // Invoke the setter method
        try {
          getPropertyUtils().setProperty(target, name, newValue);
        } catch (final NoSuchMethodException e) {
            throw new InvocationTargetException
                (e, "Cannot set " + propName);
        }

    }

断点进入到这里,PropertyDescriptor是属性描述器,接着进入到getPropertyDescriptor方法中,这个方法才是真正获取属性描述器的。

3. 进入到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;
        }

         //====================省略======================

        return result;

    }

可以看到这个getIntrospectionData这个方法和descriptor有关,那么这个方法到底做了什么呢?

4. 进入到getIntrospectionData方法干中


    private BeanIntrospectionData getIntrospectionData(final Class beanClass) {
        if (beanClass == null) {
            throw new IllegalArgumentException("No bean class specified");
        }

        // Look up any cached information for this bean class
        BeanIntrospectionData data = descriptorsCache.get(beanClass);
        if (data == null) {
            data = fetchIntrospectionData(beanClass);
            descriptorsCache.put(beanClass, data);
        }

        return data;
    }

这个方法很短,标红的部分说明了这个data会从cache中获取,如果cache中没有才会进行某种操作来获取到这个data。

那么首次运行的时候data是null的(PS:如果不是null,可以手动设置为null)

5.进入到fetchIntrospectionData方法中


    private BeanIntrospectionData fetchIntrospectionData(final Class beanClass) {
        final DefaultIntrospectionContext ictx = new DefaultIntrospectionContext(beanClass);

        for (final BeanIntrospector bi : introspectors) {
            try {
                bi.introspect(ictx);
            } catch (final IntrospectionException iex) {
                log.error("Exception during introspection", iex);
            }
        }

        return new BeanIntrospectionData(ictx.getPropertyDescriptors());
    }

这个方法中有一个  bi.introspect(ictx);方法,此方法是一个抽象方法,那么直接看它的实现类DefaultBeanIntrospector

6. 进入到类DefaultBeanIntrospector的introspect方法中,


    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;
        //=====================省略==================
        }

BeanInfo是一个接口,标红的地方是有一个getBeanInfo接口

7. 进入到getBeanInfo方法中


    public static BeanInfo getBeanInfo(Class beanClass)
        throws IntrospectionException
    {
        if (!ReflectUtil.isPackageAccessible(beanClass)) {
            return (new Introspector(beanClass, null, USE_ALL_BEANINFO)).getBeanInfo();
        }
        ThreadGroupContext context = ThreadGroupContext.getContext();
        BeanInfo beanInfo;
        synchronized (declaredMethodCache) {
            beanInfo = context.getBeanInfo(beanClass);
        }
        if (beanInfo == null) {
            beanInfo = new Introspector(beanClass, null, USE_ALL_BEANINFO).getBeanInfo();
            synchronized (declaredMethodCache) {
                context.putBeanInfo(beanClass, beanInfo);
            }
        }
        return beanInfo;
    }

在进入到Introspector类的getBeanInfo方法中


  private BeanInfo getBeanInfo() throws IntrospectionException {

        // the evaluation order here is import, as we evaluate the
        // event sets and locate PropertyChangeListeners before we
        // look for properties.
        BeanDescriptor bd = getTargetBeanDescriptor();
        MethodDescriptor mds[] = getTargetMethodInfo();
        EventSetDescriptor esds[] = getTargetEventInfo();
        PropertyDescriptor pds[] = getTargetPropertyInfo();

        int defaultEvent = getTargetDefaultEventIndex();
        int defaultProperty = getTargetDefaultPropertyIndex();

        return new GenericBeanInfo(bd, esds, defaultEvent, pds,
                        defaultProperty, mds, explicitBeanInfo);

    }

标红部分对PropertyDescriptor数组进行了初始化,进入到这个方法中


private PropertyDescriptor[] getTargetPropertyInfo() {
            //===============省略=========================

                try {

                    if (argCount == 0) {
                        if (name.startsWith(GET_PREFIX)) {
                            // Simple getter
                            pd = new PropertyDescriptor(this.beanClass, name.substring(3), method, null);
                        } else if (resultType == boolean.class && name.startsWith(IS_PREFIX)) {
                            // Boolean getter
                            pd = new PropertyDescriptor(this.beanClass, name.substring(2), method, null);
                        }
                    } else if (argCount == 1) {
                        if (int.class.equals(argTypes[0]) && name.startsWith(GET_PREFIX)) {
                            pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, method, null);
                        } else if (void.class.equals(resultType) && name.startsWith(SET_PREFIX)) {
                            // Simple setter
                            pd = new PropertyDescriptor(this.beanClass, name.substring(3), null, method);
                            if (throwsException(method, PropertyVetoException.class)) {
                                pd.setConstrained(true);
                            }
                        }
                  //==============省略==============

        return result;
    }

这个地方的截图如下:


接下的断点不深入了,这个可以看到name是getSRowIndex,那么name.subString(3)的就是 SRowIndex,而属性的名称是sRowIndex,虽然只是一个字母的大小写问题,但是会导致这个bug的出现。

解决办法

在项目中使用的是Lombok的@Data注解生成get和set方法的,生成的getSet方法如下:

可以看到这get,set方法SR是大写的,这样就导致了属性不匹配,在反射的时候调用get,set方法的时候不能完全匹配,导致属性值为null。

这种情况可以自己写该属性的get,set方法,自己写的getSet方法会覆盖Lombok自动生成的

    public void setsRowIndex(Integer sRowIndex) {
        this.sRowIndex = sRowIndex;
    }

    public Integer getsRowIndex() {
        return sRowIndex;
    }

同时,如果使用Lombok自动生成的getSet方法,在返回前台的时候,该属性会被映射为srowIndex。

解决办法是使用手动写的get方法。使用  @JsonProperty(name='sRowIndex')会导致前台返回生成两个属性(srowIndex和sRowIndex)。