Lombok让Spring和Apache的Map转Bean工具类失效?

本文探讨了Lombok的@Accessors(chain=true)注解如何导致Spring和Apache的Map转Bean工具失效。通过分析源码,解释了BeanUtils在查找setter方法时的逻辑,以及Lombok生成的链式set方法不被识别的问题,从而揭示了失效的原因。结论指出,这不是Lombok的问题,而是两者使用方式的冲突。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Lombok让Spring和Apache的Map转Bean工具类失效?

0. 背景

最近开发用到一个小功能,将Map转为Java Bean对象,于是寻找了下工具类,主要是有两个:

1. org.springframework.cglib.beans.BeanMap
2. org.apache.commons.beanutils.BeanUtils

在使用过程中遇到了一个奇怪的现象,Map转Bean后,Bean中属性值都是null。而碰巧的是上午还是ok的下午就GG了。于是想了一下中途唯一变动的就是使用了Lombok的一个注解,造成了Map转Bean失效。

笔者这里不是甩锅给Lombok,个人是很喜欢用Lombok,代码简洁,程序本身应该是一门技术也应该是一门艺术。很多人喜欢说Lombok有坑,笔者认为所谓的坑在于不清楚、不理解其中的本质。

好了,基于遇到的这个问题,笔者阅读了工具类的源码想借此了解、学习下为何会有这样的现象,顺便学习下别人的代码。下文可能会有点枯燥,毕竟是看源码,笔者尽自己所能讲清楚、记录清楚。也可以直接查看结论

1. 先上代码

1.1 pom

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
    <dependency>
        <groupId>commons-beanutils</groupId>
        <artifactId>commons-beanutils</artifactId>
        <version>1.9.4</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.3.9</version>
    </dependency>
</dependencies>

1.2 实体类

这里记住两点:

  1. 使用了Lombok的注解;
  2. 注意这个注解@Accessors(chain = true),就是这个注解造成了工具类失效,下面演示。使用这个注解是为了链式编程,可以连续调用set方法,方便使用。
/**
 * @ClassName CupEntity
 * @Description 杯子实体类
 * @Author Nile(576109623 @ qq.com)
 * @Date 22:46 2021/11/9
 * @Version 1.0
 */
@Data
@Accessors(chain = true)
public class CupEntity {
    /**
     * 颜色
     */
    private String color;
    /**
     * 容积,单位ml
     */
    private Integer capacity;
    /**
     * 材质
     */
    private String material;
    /**
     * 高度,单位cm
     */
    private Integer height;
    /**
     * 价格,单位元
     */
    private Integer price;
}

不使用@Accessors(chain = true),set方法为:

public void setColor(String color) {
	this.color = color;
}

使用@Accessors(chain = true),set方法为

public CupEntity setColor(String color) {
    this.color = color;
    return this;
}

1.3 MapToBean

import org.apache.commons.beanutils.BeanUtils;
import org.springframework.cglib.beans.BeanMap;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName MapToBean
 * @Description MapToBean
 * @Author Nile(576109623 @ qq.com)
 * @Date 22:51 2021/11/9
 * @Version 1.0
 */
public class MapToBean {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
        Map<String, Object> params = new HashMap<>();
        params.put("color", "blue");
        params.put("capacity", 300);
        params.put("material", "glass");

        // org.springframework.cglib.beans.BeanMap
        CupEntity springCup = new CupEntity();
        BeanMap beanMap = BeanMap.create(springCup);
        beanMap.putAll(params);
        springCup.setHeight(15).setPrice(12);
        System.out.println(springCup);

        // org.apache.commons.beanutils.BeanUtils
        CupEntity apacheCup = new CupEntity();
        BeanUtils.populate(apacheCup, params);
        apacheCup.setHeight(20).setPrice(8);
        System.out.println(apacheCup);
    }
}

1.4 结果

不使用@Accessors(chain = true)(此时set方法不能链式调用)

CupEntity(color=blue, capacity=300, material=glass, height=15, price=12)
CupEntity(color=blue, capacity=300, material=glass, height=20, price=8)

使用@Accessors(chain = true)

CupEntity(color=null, capacity=null, material=null, height=15, price=12)
CupEntity(color=null, capacity=null, material=null, height=20, price=8)

ok,以上就是笔者写的测试代码,下面以BeanUtils为例,分析下BeanUtils#populate的源码。(其实也想看看BeanMap的源码的,源码文件折腾半天一直没导进来。。后面有时间再补充吧)

2. BeanUtils

有兴趣跟笔者一起看源码的,请自行打开IDE对照查看源码,因代码嵌套、数据校验等问题,部分代码被笔者省略。

2.1 invoke

package org.apache.commons.beanutils;
// class BeanUtils

public static void populate(final Object bean,
                            final Map<String, ? extends Object> properties)
    throws IllegalAccessException, InvocationTargetException {
    // 这里返回一个BeanUtilsBean实例,调用其populate方法
	BeanUtilsBean.getInstance().populate(bean, properties);
}
package org.apache.commons.beanutils;
// class BeanUtilsBean

public void populate(final Object bean, final Map<String, ? extends Object> properties)
        throws IllegalAccessException, InvocationTargetException {
	// 校验
    // Do nothing unless both arguments have been specified
    if ((bean == null) || (properties == null)) {
        return;
    }
    if (log.isDebugEnabled()) {
        log.debug("BeanUtils.populate(" + bean + ", " +
                  properties + ")");
    }

    // 遍历Map,取出key和value,调用setProperty方法逐个赋值
    // Loop through the property name/value pairs to be set
    for(final Map.Entry<String, ? extends Object> 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());
    }
}


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

    // 日志记录,省略

    // 校验、解析name和value,省略

    // 调用工具类PropertyUtilsBean.setProperty
    // Invoke the setter method
    try {
        getPropertyUtils().setProperty(target, name, newValue);
    } catch (final NoSuchMethodException e) {
        throw new InvocationTargetException (e, "Cannot set " + propName);
    }
}

package org.apache.commons.beanutils;
// class PropertyUtilsBean

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

public void setNestedProperty(Object bean, String name, final Object value)
    throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
	
    // 校验、解析bean和name,省略

    if (bean instanceof Map) {
        setPropertyOfMapBean(toPropertyMap(bean), name, value);
    } else if (resolver.isMapped(name)) {
        setMappedProperty(bean, name, value);
    } else if (resolver.isIndexed(name)) {
        setIndexedProperty(bean, name, value);
    } else {
        // 普通Java Bean,走这个分支赋值
        setSimpleProperty(bean, name, value);
    }
}

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

    // 校验bane、name,省略

    // getPropertyDescriptor是关键,工具类失效的原理也在其中,这个方法如其命名,获取参数的描述
    // Retrieve the property setter method for the specified property
    final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
    if (descriptor == null) {
        throw new NoSuchMethodException("Unknown property '" +
                                        name + "' on class '" + bean.getClass() + "'" );
    }
    // 获取写方法,即set方法
    final Method writeMethod = getWriteMethod(bean.getClass(), descriptor);
    if (writeMethod == null) {
        throw new NoSuchMethodException("Property '" + name 
                                        + "' has no setter method in class '" 
                                        + bean.getClass() + "'");
    }

    // Call the property setter method
    final Object[] values = new Object[1];
    values[0] = value;
    
    // 日志记录,省略
    
    // 实际赋值方法,反射
    invokeMethod(writeMethod, bean, values);
}

好了,赋值到此就结束,通过代码的分析解读,关键在于没有获取到writeMethod,即set方法。而其中的关键就在于getPropertyDescriptor方法,所以接下来笔者继续来分析解析、获取Bean参数描述的代码。

2.2 getPropertyDescriptor

package org.apache.commons.beanutils;
// class PropertyUtilsBean

public PropertyDescriptor getPropertyDescriptor(Object bean, String name)
    throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
	
    // 校验、解析bean和name,省略

    // 调用getIntrospectionData获取属性描述
    final BeanIntrospectionData data = getIntrospectionData(bean.getClass());
    PropertyDescriptor result = data.getDescriptor(name);
    if (result != null) {
        return result;
    }
    
    // 其他情况获取属性描述,省略
    
    return result;
}

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

    // 这里使用descriptorsCache缓存Bean的内部数据
    // 第一次调用此方法时会调用下面的fetchIntrospectionData,获取Bean的内部数据
    // 在第二次给成员变量赋值时,直接从缓存中返回data
    // 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;
}

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());
}

这里的for循环中的BeanIntrospector笔者理解为内部解析器,introspectors一共有两个:

introspectors

从名字上看,第二个是用于解析废除属性的,我们关注第一个。

2.3 Introspector

package org.apache.commons.beanutils;
// class DefaultBeanIntrospector

public void introspect(final IntrospectionContext icontext) {
    // 获取Bean信息
    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;
    }

    // 保存Bean信息
    PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
    if (descriptors == null) {
        descriptors = new PropertyDescriptor[0];
    }

    handleIndexedPropertyDescriptors(icontext.getTargetClass(),
                                     descriptors);
    icontext.addPropertyDescriptors(descriptors);
}
public static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
    // 校验,以及从context中判断是否存在,省略
    
    if (beanInfo == null) {
        // 这里会实例化一个Introspector,然后调用其getBeanInfo方法
        beanInfo = new Introspector(beanClass, null, USE_ALL_BEANINFO).getBeanInfo();
        synchronized (declaredMethodCache) {
            context.putBeanInfo(beanClass, beanInfo);
        }
    }
    return beanInfo;
}

这里存在一个递归调用,在实例化Introspector的时候会调用getBeanInfo(Class<?> beanClass),获取beanClass父类的Bean信息,只到最顶层,在这里的父类直接就是Object。

这里放张图,Introspector,其中有两个属性是后续需要关注的,这里提前记录下:propertiespdStore

Introspector

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();
    // 获取属性信息,这一步执行后,可以看到properties和pdStore被赋值
    PropertyDescriptor pds[] = getTargetPropertyInfo();

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

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

}

properties&pdStore

properties和pdStroe只是数据类型不同,内容其实是相同的

以color参数为例,其PropertyDescriptor是同个实例

那我们接下来看下PropertyDescriptor中有什么:

PropertyDescriptor

好了,问题来了,Why?,为何writeMethodName为null。这是在使用了Lombok的@Accessors(chain = true)后,writeMethodName没有找到。这里就是导致 本文2.1 最后获取写方法为null的直接原因。

final Method writeMethod = getWriteMethod(bean.getClass(), descriptor);
if (writeMethod == null) {
    throw new NoSuchMethodException("Property '" + name 
                                    + "' has no setter method in class '" 
                                    + bean.getClass() + "'");
}

那么我们接下来要寻找的就是如何设置writeMethodName。我们来看getTargetPropertyInfo方法:

private PropertyDescriptor[] getTargetPropertyInfo() {

    // 校验,省略

    if (explicitProperties != null) {
        // Add the explicit BeanInfo data to our results.
        addPropertyDescriptors(explicitProperties);

    } else {

        // Apply some reflection to the current class.

        // First get an array of all the public methods at this level
        Method methodList[] = getPublicDeclaredMethods(beanClass);

        // 遍历类中所有的方法(包括父类Object的方法)
        // Now analyze each method.
        for (int i = 0; i < methodList.length; i++) {
            Method method = methodList[i];
            if (method == null) {
                continue;
            }
            // skip static methods.
            int mods = method.getModifiers();
            if (Modifier.isStatic(mods)) {
                continue;
            }
            String name = method.getName();
            Class<?>[] argTypes = method.getParameterTypes();
            Class<?> resultType = method.getReturnType();
            int argCount = argTypes.length;
            PropertyDescriptor pd = null;

            if (name.length() <= 3 && !name.startsWith(IS_PREFIX)) {
                // Optimization. Don't bother with invalid propertyNames.
                continue;
            }

            try {
				// 这部分代码放到下一个代码块中,方便阅读
                if (argCount == 0) {
                    // 没有参数,省略;readMethod就是在这里获取到的,我们这里不关注
                } else if (argCount == 1) {
                    // 一个参数,这里获取writeMethod
                } else if (argCount == 2) {
                    // 2个参数,省略
                }
            } catch (IntrospectionException ex) {
                pd = null;
            }

			// other,省略
        }
    }
    // 将获取到的PropertyDescriptor存入pdStroe中,并更新实体类Introspector的属性properties
    processPropertyDescriptors();

    // other,省略

    return result;
}

获取PropertyDescriptor

if (argCount == 0) {
	// 没有参数,省略;readMethod就是在这里获取到的,我们这里不关注
} else if (argCount == 1) {
    // 一个参数,这里获取writeMethod
    
    if (int.class.equals(argTypes[0]) && name.startsWith(GET_PREFIX)) {
        pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null,
                                           method, null);
	// 好了,终于到头了。
    // static final String SET_PREFIX = "set";
    // 判断是否返回类型为void,并且方法名以"set"开头;是则构建PropertyDescriptor,并赋值给pd
    } 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);
        }
    }
} else if (argCount == 2) {
	// 2个参数,省略
}

好了,终于到头了,到了这里就明白了为何使用Lombok的@Accessors(chain = true)没能找到writeMethod,因为其set方法为了能够链式编程,返回了当前对象

看到这么判断,是不是突然觉得还是挺简单的。

3. 结论

Lombok让Spring和Apache的Map转Bean工具类失效??Lombok说:“这锅我不背!”

使用Lombok的@Accessors(chain = true)实现链式编程,会让其set方法返回当前对象。

public CupEntity setColor(String color) {
    this.color = color;
    return this;
}

org.apache.commons.beanutils.BeanUtils在解析Bean时,只将返回类型为void,并且方法名以"set"开头的方法认定为writeMethod

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);
    }
}

这两者存在冲突,导致writeMethod为null,最终导致Map转Bean失效。

(解决方案就不写了吧…)

4. 前人的肩膀

放两个链接,介绍了如何使用Lombok以及Builder模式:

那么,各位路过的朋友们,你们怎么看Lombok呢?认为它有坑,不要使用;还是一直用一直爽呢?

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值