opengoofy/crane4j

当通过 `@AutoOperate#on` 从返回值中提取待填充数据时,会偶发空指针异常

Closed this issue · 3 comments

java.lang.NullPointerException: null
        at cn.crane4j.core.util.CollectionUtils.computeIfAbsent(CollectionUtils.java:430)
        at cn.crane4j.core.util.ReflectUtils.getDeclaredFields(ReflectUtils.java:472)
        at cn.crane4j.core.util.ReflectUtils.lambda$null$12(ReflectUtils.java:499)
        at cn.crane4j.core.util.ReflectUtils.traverseTypeHierarchy(ReflectUtils.java:439)
        at cn.crane4j.core.util.ReflectUtils.lambda$getFields$13(ReflectUtils.java:499)
        at cn.crane4j.core.util.CollectionUtils.computeIfAbsent(CollectionUtils.java:430)
        at cn.crane4j.core.util.ReflectUtils.getFields(ReflectUtils.java:497)
        at cn.crane4j.core.util.ReflectUtils.getField(ReflectUtils.java:484)
        at cn.crane4j.core.support.reflect.ReflectivePropertyOperator$ReflectivePropDesc.findGetterMethod(ReflectivePropertyOperator.java:298)
        at cn.crane4j.core.support.reflect.ReflectivePropertyOperator$ReflectivePropDesc.findGetter(ReflectivePropertyOperator.java:129)
        at cn.crane4j.core.support.reflect.AbstractPropDesc.lambda$obtainInvokerFromCache$1(AbstractPropDesc.java:60)
        at cn.crane4j.core.util.CollectionUtils.computeIfAbsent(CollectionUtils.java:430)
        at cn.crane4j.core.support.reflect.AbstractPropDesc.obtainInvokerFromCache(AbstractPropDesc.java:59)
        at cn.crane4j.core.support.reflect.AbstractPropDesc.getGetter(AbstractPropDesc.java:35)
        at cn.crane4j.core.support.reflect.PropertyOperator.findGetter(PropertyOperator.java:52)
        at cn.crane4j.core.support.reflect.PropertyOperator.readProperty(PropertyOperator.java:39)
        at cn.crane4j.core.support.reflect.ChainAccessiblePropertyOperator.lambda$chainGetter$1(ChainAccessiblePropertyOperator.java:99)
        at cn.crane4j.core.support.auto.DefaultAutoOperateAnnotatedElement.execute(DefaultAutoOperateAnnotatedElement.java:70)
        at cn.crane4j.core.support.aop.MethodResultAutoOperateSupport.afterMethodInvoke(MethodResultAutoOperateSupport.java:80)
        at cn.crane4j.extension.spring.aop.MethodResultAutoOperateAdvisor.invoke(MethodResultAutoOperateAdvisor.java:56)

在新增数据之后,立即调用查询方法,间隔时间比较端,在毫秒级别,就会有一定概率出现这个异常

同样的场景还会偶发异常:

java.lang.NegativeArraySizeException: -1
        at java.base/java.util.HashSet.toArray(HashSet.java:368)
        at java.base/java.util.LinkedList.addAll(LinkedList.java:412)
        at java.base/java.util.LinkedList.addAll(LinkedList.java:391)
        at cn.crane4j.core.util.CollectionUtils.addAll(CollectionUtils.java:269)
        at cn.crane4j.core.util.ReflectUtils.traverseTypeHierarchy(ReflectUtils.java:444)
        at cn.crane4j.core.util.ReflectUtils.lambda$getFields$13(ReflectUtils.java:499)
        at cn.crane4j.core.util.CollectionUtils.computeIfAbsent(CollectionUtils.java:430)
        at cn.crane4j.core.util.ReflectUtils.getFields(ReflectUtils.java:497)
        at cn.crane4j.core.util.ReflectUtils.getField(ReflectUtils.java:484)
        at cn.crane4j.core.support.reflect.ReflectivePropertyOperator$ReflectivePropDesc.findGetterMethod(ReflectivePropertyOperator.java:298)
        at cn.crane4j.core.support.reflect.ReflectivePropertyOperator$ReflectivePropDesc.findGetter(ReflectivePropertyOperator.java:129)
        at cn.crane4j.core.support.reflect.AbstractPropDesc.lambda$obtainInvokerFromCache$1(AbstractPropDesc.java:60)
        at cn.crane4j.core.util.CollectionUtils.computeIfAbsent(CollectionUtils.java:430)
        at cn.crane4j.core.support.reflect.AbstractPropDesc.obtainInvokerFromCache(AbstractPropDesc.java:59)
        at cn.crane4j.core.support.reflect.AbstractPropDesc.getGetter(AbstractPropDesc.java:35)
        at cn.crane4j.core.support.reflect.PropertyOperator.findGetter(PropertyOperator.java:52)
        at cn.crane4j.core.support.reflect.PropertyOperator.readProperty(PropertyOperator.java:39)
        at cn.crane4j.core.support.reflect.ChainAccessiblePropertyOperator.lambda$chainGetter$1(ChainAccessiblePropertyOperator.java:99)
        at cn.crane4j.core.support.auto.DefaultAutoOperateAnnotatedElement.execute(DefaultAutoOperateAnnotatedElement.java:70)
        at cn.crane4j.core.support.aop.MethodResultAutoOperateSupport.afterMethodInvoke(MethodResultAutoOperateSupport.java:80)
        at cn.crane4j.extension.spring.aop.MethodResultAutoOperateAdvisor.invoke(MethodResultAutoOperateAdvisor.java:56)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:752)
        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:752)
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:703)

第一个问题看起来是由于 ReflectUtils#getDeclaredFields 方法入参为 null 引起的,不过光靠这个堆栈很难排查出为什么一条路传下来冒出来了一个 null。第二个问题目前还没有什么头绪,感觉可能跟 JDK 版本有关系。

这两个问题我本地都没有复现出来,能否把涉及到的相关代码发上来呢?包括触发填充的方法和它返回的类(即被 @AutoOperate 注解的方法),被填充的类以及它的父类和接口(如果有的话),如果有可运行的实例代码就更好了。

@feiyue

目前能确认的是,NegativeArraySizeException 这个问题是由一个 HashSet 的并发修改问题引起的。

具体来说,当进行反射的时候,都会调用 ReflectUtils#traverseTypeHierarchy 这个方法遍历类与其层级结构:

public static void traverseTypeHierarchy(Class<?> beanType, Consumer<Class<?>> consumer) {
      Set<Class<?>> accessed = new HashSet<>();
      Deque<Class<?>> typeQueue = new LinkedList<>();
      typeQueue.add(beanType);
      while (!typeQueue.isEmpty()) {
          Class<?> type = typeQueue.removeFirst();
          accessed.add(type);
          // do something for current type
          consumer.accept(type);
          // then find superclass and interfaces
          Set<Class<?>> declaredSuperClassWithInterface = getDeclaredSuperClassWithInterface(type); // 获取父类与接口
          declaredSuperClassWithInterface.remove(Object.class);
          declaredSuperClassWithInterface.removeAll(accessed);
          CollectionUtils.addAll(typeQueue, declaredSuperClassWithInterface);
      }
  }

问题在于,getDeclaredSuperClassWithInterface 这个方法获取的 Set 集合实际上是从缓存中获取的:

public static Set<Class<?>> getDeclaredSuperClassWithInterface(Class<?> type) {
      return CollectionUtils.computeIfAbsent(DECLARED_SUPER_CLASS_WITH_INTERFACE, type, k -> {
          Set<Class<?>> result = new LinkedHashSet<>();
          Class<?> superClass = type.getSuperclass();
          if (superClass != null) {
              result.add(superClass);
          }
          result.addAll(Arrays.asList(type.getInterfaces()));
          return result;
      });
  }

而在 traverseTypeHierarchy 方法中又修改了获取的 Set 集合,在并发环境下,就又可能出现 Set 内部持有的 Map 集合因为并发操作,最终出现 size 被扣减到 0 以下的情况,此后再调用 Set.toArray 就会出现 NegativeArraySizeException 异常。

不过,上面的空指针目前仍然不能确认是什么原因造成的,只能根据堆栈初步判断是在通过 traverseTypeHierarchy 方法遍历类层级结构的时候出现了一个 null,所以目前怀疑可能也是因为这个并发修改的问题导致的。

这周内会发布 2.8.1 先修复 NegativeArraySizeException 这个问题,升级完以后需要再观察一下空指针这个问题是否会再出现。