lombok的@Data导致StackOverflowError
最近在写代码实现循环依赖问题的时候遇到了一个问题 , 已经解决了循环依赖的问题 , 同时Debug代码也确实按照原本构思的流程走了, 但是在最后执行的时候莫名奇妙出现了栈溢出异常
1 | BeanA beanA = (BeanA)applicationContext.getBean("beanA"); |
原本还以为在抄代码的时候哪里抄错了 , 导致循环依赖的问题没有解决
不过经过反复的Debug以及手动画流程, 基本可以确定代码逻辑是没有问题的
接着使用了项目作者给出的测试数据 , 并没有出现异常 , 但是在debug到最后准备打印数据的时候 , IDEA的调试器显示出来有一个StackOverflowError
经过反复的问题排查, 最后可以确定问题出在lombok的@Data
上
这里先给出代码使用到的类
BeanA
1 |
|
BeanB
1 |
|
接着访问官网 , 确定@Data
的具体作用
All together now: A shortcut for @ToString, @EqualsAndHashCode, @Getter on all fields, @Setter on all non-final fields, and @RequiredArgsConstructor!
注意 , 这里有@EqualsAndHashCode
注解
接着我们查看 编译后的代码字节码文件(经过反编译)
BeanA的hashCode
1 | public int hashCode() { |
BeanB的hashCode
1 | public int hashCode() { |
仍然是由于循环依赖的问题 , 这里会无限的去调用 两个类的hashCode()
方法
a.hashCode()=>b.hashCode()=>a.hashCode()=>b.hashCode()=>a.hashCode()=>b.hashCode() …
如此也就导致了死循环
为什么IDEA的调试器可以看到 StackOverflowerError
这里是因为在调试器中我们看到的 对象
实际上是IDEA的调试器执行了 对象 的toString()
方法
这里通过下面的代码可以简单证实
1 | public static void main(String[] args) throws Exception { |
调试代码, 可以看到 person = "123"
这也就解释了为什么我们可以在调试器中看到 出现了 error , 但是如果没有执行对应的代码, 却不会出现错误
为什么toString方法导致栈溢出?
前面我们提到 hashCode方法会无限的调用, 这里toString()方法也是一样的 , 无限的调用导致JVM方法栈移除 , 出现error
那么再次回顾我们的代码
1 | static void circleDepeTest() throws Exception { |
这里出现栈溢出的关键在于 System.out.println(beanA);
下面给出主要设计到的执行的方法
1 | public void println(Object x) { |
String.valueOf()
1 | public static String valueOf(Object obj) { |
这里我们使用了@Data
, 默认生成了toString
方法
BeanA的反编译代码
1 | public String toString() { |
这里执行了一个字符串的拼接操作 , 由于getBeanB()返回了BeanB的对象 , 拼接字符操作执行beanB的toString()方法
BeanB的反编译代码
1 | public String toString() { |
那么循环调用就发生在这里 , 又开始执行beanA的toString()方法 , 也就导致死循环 , 无限调用下 , 导致出现栈溢出错误
关于Java方法栈
我们都知道方法的调用是一层一层的 , 结合异常抛出的结果 , 也非常容易理解
比如下面的代码
1 | class Test{ |
这里执行到方法C的时候会抛出异常
1 | Exception in thread "main" java.lang.ArithmeticException: / by zero |
由于在a b c方法中我们都没有处理这个抛出的异常 , 一直到main方法里面抛出 , 于是异常在控制台中显示 , 程序执行终止 , 我们也无法看到success
的字符串
关于方法栈, 也非常容易理解 , 参考下图调试器中的方法调用 , 执行的顺序为
main -> a -> b -> c
这里的每一条数据可以理解为栈帧
栈帧:一个栈帧随着一个方法的调用开始而创建,这个方法调用完成而销毁。栈帧内存放者方法中的局部变量,操作数栈等数据。
Java栈也称作虚拟机栈(Java Vitual Machine Stack),JVM栈只对栈帧进行存储,压栈和出栈操作。Java栈是Java方法执行的内存模型。
总结
慎用@Data
, 使用@Setter @Getter
等即可
别的方面 , 比如
@Setter
方法的问题 , 变量aName 的set方法是setAName
, 这里显然不符合常见的命名规范 , 也可能会诱导别的问题