cglib动态代理后的实例在尝试获取所实现接口的范型信息时出现循环递归导致栈溢出

现象描述

正常启动服务时,突然遇到栈溢出异常,日志输出内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
java.lang.StackOverflowError: null
at org.springframework.data.util.TypeDiscoverer.createInfo(TypeDiscoverer.java:113) ~[spring-data-commons-2.5.1.jar:2.5.1]
at org.springframework.data.util.TypeDiscoverer.getSuperTypeInformation(TypeDiscoverer.java:443) ~[spring-data-commons-2.5.1.jar:2.5.1]
at org.springframework.data.util.ClassTypeInformation.getSuperTypeInformation(ClassTypeInformation.java:42) ~[spring-data-commons-2.5.1.jar:2.5.1]
at org.springframework.data.util.TypeDiscoverer.getSuperTypeInformation(TypeDiscoverer.java:449) ~[spring-data-commons-2.5.1.jar:2.5.1]
at org.springframework.data.util.ClassTypeInformation.getSuperTypeInformation(ClassTypeInformation.java:42) ~[spring-data-commons-2.5.1.jar:2.5.1]
at org.springframework.data.util.TypeDiscoverer.getSuperTypeInformation(TypeDiscoverer.java:449) ~[spring-data-commons-2.5.1.jar:2.5.1]
at org.springframework.data.util.ClassTypeInformation.getSuperTypeInformation(ClassTypeInformation.java:42) ~[spring-data-commons-2.5.1.jar:2.5.1]
at org.springframework.data.util.TypeDiscoverer.getSuperTypeInformation(TypeDiscoverer.java:449) ~[spring-data-commons-2.5.1.jar:2.5.1]
at org.springframework.data.util.ClassTypeInformation.getSuperTypeInformation(ClassTypeInformation.java:42) ~[spring-data-commons-2.5.1.jar:2.5.1]
at org.springframework.data.util.TypeDiscoverer.getSuperTypeInformation(TypeDiscoverer.java:449) ~[spring-data-commons-2.5.1.jar:2.5.1]
...

排查过程

日志中能得到的有效信息只有栈溢出和TypeDiscoverer,根本没有实际的调用栈。

这时候就得借助调试工具,我比较习惯用IDEA的breakpoint断点工具

然后以debug的方式启动系统就可以定位到具体的源码了。

通过IDEA的栈查看器来查看和定位上下文

直接跳转到具体出现问题的源码:

1
2
3
4
5
6
7
8
9
Map<Class,Map<Class,List<WorkerDoneListener>>> listenerMap = new HashMap<>();  
if(listeners != null){
for (WorkerDoneListener l : listeners) {
List<TypeInformation<?>> types = ClassTypeInformation.from(l.getClass()).getRequiredSuperTypeInformation(WorkerDoneListener.class).getTypeArguments();
Class request = types.get(0).getType();
Class response = types.get(1).getType();
listenerMap.computeIfAbsent(request,k->new HashMap<>()).computeIfAbsent(response,k->new ArrayList<>()).add(l);
}
}

这段代码中,listeners是通过Spring依赖注入的接口WorkerDoneListener的实现类集合。

其中真正出问题的代码是

1
List<TypeInformation<?>> types = ClassTypeInformation.from(l.getClass()).getRequiredSuperTypeInformation(WorkerDoneListener.class).getTypeArguments();  

这行代码正试图获取接口WorkerDoneListener的实现类的范型数据。

这里我贴一下接口WorkerDoneListener的定义:

1
2
3
public interface WorkerDoneListener<T,R>{  
// ...省略
}

其中一个实现类的定义(类型均做了脱敏):

1
2
3
public class AService implements WorkerDoneListener<RequestA,ResponseA>{
// ...省略
}

**其实就是想拿到AService中两个范型RequestAResponseA**。

getRequiredSuperTypeInformation方法是接口org.springframework.data.util.TypeInformation提供的,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
default TypeInformation<?> getRequiredSuperTypeInformation(Class<?> superType) {  

TypeInformation<?> result = getSuperTypeInformation(superType);

if (result == null) {
throw new IllegalArgumentException(String.format(
"Can't retrieve super type information for %s! Does current type really implement the given one?",
superType));
}

return result;
}

很明显,这里没有递归。所以问题应该就是因为

1
TypeInformation<?> result = getSuperTypeInformation(superType);  

这行代码,其对应的源码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public TypeInformation<?> getSuperTypeInformation(Class<?> superType) {  
Class<?> rawType = getType();
if (!superType.isAssignableFrom(rawType)) {
return null;
}
if (getType().equals(superType)) {
return this;
}
List<Type> candidates = new ArrayList<>();
Type genericSuperclass = rawType.getGenericSuperclass();
if (genericSuperclass != null) {
candidates.add(genericSuperclass);
}
candidates.addAll(Arrays.asList(rawType.getGenericInterfaces()));
for (Type candidate : candidates) {
TypeInformation<?> candidateInfo = createInfo(candidate);
if (superType.equals(candidateInfo.getType())) {
return candidateInfo;
} else {
TypeInformation<?> nestedSuperType = candidateInfo.getSuperTypeInformation(superType);
if (nestedSuperType != null) {
return nestedSuperType;
}
}
}
return null;
}

可以看到,这里和日志中对应上的,通过分析之后我发现主要是因为

1
TypeInformation<?> nestedSuperType = candidateInfo.getSuperTypeInformation(superType);  

这行代码导致的递归调用

我认真排查了这个方法以后,最终确认问题点在于

1
candidates.addAll(Arrays.asList(rawType.getGenericInterfaces()));  

这一行代码中的

1
rawType.getGenericInterfaces()

方法的调用,我通过debug调试,发现这个方法返回的是:

根本就没有他实际实现的接口信息,再加上递归查询的时候,仍然传递的是superType这个参数,而superType没有发生任何变化,所以就造成了递归,最终导致了栈溢出。


那么为什么rawType.getGenericInterfaces()方法无法获取到其实现的接口方法呢?

这里需要特别注意rawType实例的信息:com.xxxxx.service.AService$$EnhancerBySpringCGLIB$$87959439,可以看到这个实例是cglib动态代理生成的,问题就出在这里,因为经由cglib动态代理之后的类,是没有办法直接使用Class类提供的getGenericInterfaces()方法获取其原实现的接口信息的。

另外补充一下动态代理的两种类型信息:

  1. JDK动态代理:基于接口实现的,被代理的类仅能实现一个接口
  2. CGLIB动态代理:基于类实现的

那么为什么这个AService突然就被cglib动态代理了呢?

首先我查了一下配置相关是否有新的变更,但事后证明这个思路是错的。
应该直接看这个类的变更历史,最终发现最近一次提交中,AService的一个方法上新增了javax.transaction.Transactional注解,到这里就彻底水落石出了。

总结

  1. 栈溢出没有堆异常栈信息时,借助IDEA的Java Exception Breakpoints阻塞进行调试;
  2. 耐心分析出现错误的源码逻辑,认真、仔细的阅读,对于不理解的源码,多看多学官方文档,耐心理解;
  3. 知识的积累很重要,如果你对于动态代理的了解不够,或者说是你没有注意到rawType的实例信息,你忽视了rawType是由cglib动态代理生成的,那你可能就找不到正确的排查思路
  4. 后续再尝试获取一个类的相关信息时,务必小心动态代理带来的变化点,避免踩坑