写点什么

Lombok 注解引发的空指针问题分析

  • 2024-06-21
    北京
  • 本文字数:3032 字

    阅读完需:约 10 分钟

一、问题描述

在一次上线后,日志中出现空指针的报错,但是报错代码位置以及相应工具类未进行过修改,接下来进一步分析。


以下为报错堆栈信息:


java.lang.NullPointerException: null  at net.sf.cglib.core.ReflectUtils.getMethodInfo(ReflectUtils.java:424) ~[cglib-3.1.jar:?]  at net.sf.cglib.beans.BeanCopier$Generator.generateClass(BeanCopier.java:133) ~[cglib-3.1.jar:?]  at net.sf.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25) ~[cglib-3.1.jar:?]  at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216) ~[cglib-3.1.jar:?]  at net.sf.cglib.beans.BeanCopier$Generator.create(BeanCopier.java:90) ~[cglib-3.1.jar:?]  at net.sf.cglib.beans.BeanCopier.create(BeanCopier.java:50) ~[cglib-3.1.jar:?]  at ***.CglibBeanCopier.copyProperties(CglibBeanCopier.java:90) ~[***.jar:1.2.0]  at ***.CglibBeanCopier.copyProperties(CglibBeanCopier.java:113) ~[***.jar:1.2.0]  at ***.CglibBeanCopier.copyPropertiesOfList(CglibBeanCopier.java:123) ~[***.jar:1.2.0]    ..省略
复制代码

二、问题分析

1.分析链路长,直接抛结论

通过 Lombok 提供的功能使得我们不必在对象中显式定义 get 和 set 方法。并且 Lombok 提供链式编程,通过在对象头部加上 @Accessors(chain = true)注解,给属性赋值时,可以写成 obj.setA(a).setB(b).setC(c),省去先 new 再对属性逐个 set 赋值。使用了该注解,这个类的 set 方法返回我就不是 void 而是 this 对象本身。


@Accessors(chain = true)public class YourClass {    private int a;
@Setter public YourClass setA(int a) { this.a = a; return this; }}
复制代码


而 JDK Introspector(它为目标 JavaBean 提供了一种了解原类方法、属性和事件的标准方法)中对写入方法是有特殊判断的,截取 Introspector.getBeanInfo(beanClass)中一段源码,只有返回值是 void,且方法名以 set 作为前缀的,才会被当做 writeMethod,即写入方法。所以返回值为 void 且是“set”开头的才是 Introspector 认为的写入方法,一种狭义的定义。


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);      }   }}
复制代码


像 BeanCopier 依赖 Introspector 的 writeMethod 对目标类赋值的工具,在转换使用了 @Accessors(chain = true)注解的类时,在获取属性描述 PropertyDescriptor 就不会返回这个属性的 writeMethod 属性,就相当于该类的属性没有“写入方法”,这就造成了拷贝对象过程中出现空指针问题。

2.分析路径

List<WaybillProcessDTO> mtProcessDtoList = **WaybillProvider.getMtWayBillProcess(**);List<WaybillProcess> mtProcessList = CglibBeanCopier.copyPropertiesOfList(mtProcessDtoList, WaybillProcess.class);if(CollectionUtils.isNotEmpty(mtProcessList)) {   waybillProcessList.addAll(mtProcessList);}
复制代码


(1)通过报错信息定位到代码端,通常情况看到 mtProcessDtoList 是从服务中获取,第一印象认为对象是可能为 null,其实不然,仔细看堆栈,问题还是出在工具类里,


“***.CglibBeanCopier.copyProperties”,继续看这段代码是存在判空操作的,造成空指针的还是 copyProperties 这个方法。


public static <T> List<T> copyPropertiesOfList(List<?> sourceList, Class<T> targetClass) {    if (sourceList == null || sourceList.isEmpty()) {        return Collections.emptyList();    }    List<T> resultList = new ArrayList<>(sourceList.size());    for (Object o : sourceList) {        resultList.add(copyProperties(o, targetClass));    }    return resultList;}
复制代码


(2)具体看 copyProperties 这个代码的实现,工具类的封装的底层能力是 BeanCopier 提供的,从传参来看并没有我们常见的传 null 后对 null 进行操作引起的空指针,还需要对 BeanCopier 的源码进行分析。


public static void copyProperties(Object source, Object target) {    if(source == null || target == null) {        log.error("对象属性COPY时入参为空,source:{},target:{}",JSON.toJSONString(source), JSON.toJSONString(target));            return;    }    if(source instanceof List && target instanceof List) {            throw new ParamErrorException("请使用[copyProperties(a,b,c)]方法进行集合类的值拷贝");    }    String beanKey = generateKey(source.getClass(), target.getClass());    BeanCopier copier;    if (! beanCopierMap.containsKey(beanKey)) {        copier = BeanCopier.create(source.getClass(), target.getClass(), false);        beanCopierMap.put(beanKey, copier);     } else {        copier = beanCopierMap.get(beanKey);     }     copier.copy(source, target, null);}
复制代码


(3)由于 jar 是进行反编译的,堆栈里提供的代码行数已经失真了,直接贴上报空指针的源码截图。




getMethodInfo 入参 member 是 null,从而导致空指针。需要通过断点跟踪运行时的变量值,找到 setters 数组中的元素是如何生成的。



(4)target 是作为对象拷贝的目标对象的类,setters 这个数组就是通过反射获取该目标类的所有具备读方法的描述对象(PropertyDescriptor 对象,可以理解为属性/方法描述)。这里面方法名有些歧义,不是说只返回 getter 相关的属性对象,返回的是该类所有具备读或写方法的属性描述,两个布尔值的类型分别控制校验读或写。




综上,由于无法获取目标类的 writeMethod,从而没有办法找到这个属性的写入方法,就没有办法对目标对象继续赋值。



此时方向就转到了目标类的实现上,分析到这里就跟 Lombok 产生了联系。此处确实被修改过,WaybillProcess 类增加了 @Accessors 这个注解。


@Setter@Getter@Accessors(chain = true)public class WaybillProcess {}
复制代码


(5)WaybillProcess 使用了 @Accessors(chain = true)这个注解,这就回到了开头提到的,在使用了这个注解后该类生成的 set 方法返回值就不是 void 而是 this,在通过 Introspector 获取属性描述时就不会被认定是写入方法,在去掉这个注解后,writeMethodName 就有值了。


三、解决办法

解决办法 1:删除该注解,将工程里链式 set 改成了常规的 set 赋值方式。


解决办法 2:保留该注解,替换对象拷贝的工具类,建议使用 MapStruct 配合 Lombok,直接在编译时生成 get/set 方法,更加安全,功能也更加强大。

四、总结

凡是依赖 JDK Introspector 获取类 set 方法描述的工具类、组件都会受到其写入方法定义导致的一些列问题,目前在工程实践中遇到了 BeanCopier 进行对象拷贝、BeanUtils 对属性进行赋值都会遇到问题。所以大家在日常开发过程中,如果该类已经被大面积的使用,在使用组件特性时需要多留意。


对于对象拷贝已经有很多最佳实践了,有相关的文章大家可以推荐一下。


感谢阅读!

发布于: 刚刚阅读数: 4
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Lombok注解引发的空指针问题分析_京东科技开发者_InfoQ写作社区