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

01 问题描述
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将 在一次上线后,日志中出现空指针的报错,但是报错代码位置以及相应工具类未进行过修改,接下来进一步分析。
以下为报错堆栈信息: 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] ..省略
问题描述
java.lang.NullPointerException: nullat 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.分析链路长,直接抛结论
@Accessors(chain = true)public class YourClass {private int a;@Setterpublic YourClass setA(int a) {this.a = a;return this;}}
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 setterpd = 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);}
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是进行反编译的,堆栈里提供的代码行数已经失真了,直接贴上报空指针的源码截图。
(4)target是作为对象拷贝的目标对象的类,setters这个数组就是通过反射获取该目标类的所有具备读方法的描述对象(PropertyDescriptor对象,可以理解为属性/方法描述)。这里面方法名有些歧义,不是说只返回getter相关的属性对象,返回的是该类所有具备读或写方法的属性描述,两个布尔值的类型分别控制校验读或写。
综上,由于无法获取目标类的writeMethod,从而没有办法找到这个属性的写入方法,就没有办法对目标对象继续赋值。
此时方向就转到了目标类的实现上,分析到这里就跟Lombok产生了联系。此处确实被修改过,WaybillProcess类增加了@Accessors这个注解。
@Setter@Getter@Accessors(chain = true)public class WaybillProcess {}
解决办法
解决办法1:删除该注解,将工程里链式set改成了常规的set赋值方式。
解决办法2:保留该注解,替换对象拷贝的工具类,建议使用MapStruct配合Lombok,直接在编译时生成get/set方法,更加安全,功能也更加强大。
总结
凡是依赖JDK Introspector获取类set方法描述的工具类、组件都会受到其写入方法定义导致的一些列问题,目前在工程实践中遇到了BeanCopier进行对象拷贝、BeanUtils对属性进行赋值都会遇到问题。所以大家在日常开发过程中,如果该类已经被大面积地使用,在使用组件特性时需要多留意。
▪功能支撑:会员成长体系、等级计算策略、权益体系、营销底层能力支持
▪用户活跃:会员关怀、用户触达、活跃活动、业务线交叉获客、拉新促活