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 实体类
这里记住两点:
- 使用了Lombok的注解;
- 注意这个注解
@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一共有两个:
从名字上看,第二个是用于解析废除属性的,我们关注第一个。
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,其中有两个属性是后续需要关注的,这里提前记录下:properties
、pdStore
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和pdStroe只是数据类型不同,内容其实是相同的
以color参数为例,其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呢?认为它有坑,不要使用;还是一直用一直爽呢?