最近在写代码实现循环依赖问题的时候遇到了一个问题 , 已经解决了循环依赖的问题 , 同时Debug代码也确实按照原本构思的流程走了, 但是在最后执行的时候莫名奇妙出现了栈溢出异常

1
2
3
BeanA beanA = (BeanA)applicationContext.getBean("beanA");
beanA.showB();
System.out.println(beanA);

原本还以为在抄代码的时候哪里抄错了 , 导致循环依赖的问题没有解决

不过经过反复的Debug以及手动画流程, 基本可以确定代码逻辑是没有问题的

接着使用了项目作者给出的测试数据 , 并没有出现异常 , 但是在debug到最后准备打印数据的时候 , IDEA的调试器显示出来有一个StackOverflowError

经过反复的问题排查, 最后可以确定问题出在lombok的@Data

这里先给出代码使用到的类
BeanA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
public class BeanA {

String code;
BeanB beanB;

public void showB() {
System.out.println(beanB + "code : " + code);
}

public void setCode(String code) {
this.code = code;
}

public void setBeanB(BeanB beanB) {
this.beanB = beanB;
}
}

BeanB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
public class BeanB {

BeanA beanA;

String code;
public void showA(){
System.out.println(beanA + "code : " + code);
}

public void setBeanA(BeanA beanA) {
this.beanA = beanA;
}

public void setCode(String code) {
this.code = code;
}
}

接着访问官网 , 确定@Data的具体作用

All together now: A shortcut for @ToString, @EqualsAndHashCode, @Getter on all fields, @Setter on all non-final fields, and @RequiredArgsConstructor!

---- https://projectlombok.org/features/Data

注意 , 这里有@EqualsAndHashCode 注解

接着我们查看 编译后的代码字节码文件(经过反编译)

BeanA的hashCode

1
2
3
4
5
6
7
8
9
public int hashCode() {
int PRIME = true;
int result = 1;
Object $code = this.getCode();
result = result * 59 + ($code == null ? 43 : $code.hashCode());
Object $beanB = this.getBeanB();
result = result * 59 + ($beanB == null ? 43 : $beanB.hashCode());
return result;
}

BeanB的hashCode

1
2
3
4
5
6
7
8
9
public int hashCode() {
int PRIME = true;
int result = 1;
Object $beanA = this.getBeanA();
result = result * 59 + ($beanA == null ? 43 : $beanA.hashCode());
Object $code = this.getCode();
result = result * 59 + ($code == null ? 43 : $code.hashCode());
return result;
}

仍然是由于循环依赖的问题 , 这里会无限的去调用 两个类的hashCode()方法

a.hashCode()=>b.hashCode()=>a.hashCode()=>b.hashCode()=>a.hashCode()=>b.hashCode() …

如此也就导致了死循环

为什么IDEA的调试器可以看到 StackOverflowerError

这里是因为在调试器中我们看到的 对象

实际上是IDEA的调试器执行了 对象 的toString() 方法

这里通过下面的代码可以简单证实

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {
Person person = new Person();
person.name="fgerghert";
person.age="123";
System.out.println(person);
}

static class Person {
String name;
String age;

@Override
public String toString() {
return age;
}
}

调试代码, 可以看到 person = "123"

这也就解释了为什么我们可以在调试器中看到 出现了 error , 但是如果没有执行对应的代码, 却不会出现错误

为什么toString方法导致栈溢出?

前面我们提到 hashCode方法会无限的调用, 这里toString()方法也是一样的 , 无限的调用导致JVM方法栈移除 , 出现error

那么再次回顾我们的代码

1
2
3
4
5
6
7
static void circleDepeTest() throws Exception {
JsonApplicationContext applicationContext = new JsonApplicationContext("application.json");
applicationContext.init();
BeanA beanA = (BeanA) applicationContext.getBean("beanA");
beanA.showB();
System.out.println(beanA);
}

这里出现栈溢出的关键在于 System.out.println(beanA);

下面给出主要设计到的执行的方法

1
2
3
4
5
6
7
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}

String.valueOf()

1
2
3
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}

这里我们使用了@Data , 默认生成了toString方法

BeanA的反编译代码

1
2
3
public String toString() {
return "BeanA(code=" + this.getCode() + ", beanB=" + this.getBeanB() + ")";
}

这里执行了一个字符串的拼接操作 , 由于getBeanB()返回了BeanB的对象 , 拼接字符操作执行beanB的toString()方法

BeanB的反编译代码

1
2
3
public String toString() {
return "BeanB(beanA=" + this.getBeanA() + ", code=" + this.getCode() + ")";
}

那么循环调用就发生在这里 , 又开始执行beanA的toString()方法 , 也就导致死循环 , 无限调用下 , 导致出现栈溢出错误

关于Java方法栈

我们都知道方法的调用是一层一层的 , 结合异常抛出的结果 , 也非常容易理解

比如下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Test{

public static void main(String[] args) {
a();
System.out.println("success");
}
static void a(){
b();
}

static void b(){
c();
}

static void c(){
int a= 1/0;
}
}

这里执行到方法C的时候会抛出异常

1
2
3
4
5
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.xilidou.framework.ioc.Test.c(Test.java:24)
at com.xilidou.framework.ioc.Test.b(Test.java:20)
at com.xilidou.framework.ioc.Test.a(Test.java:16)
at com.xilidou.framework.ioc.Test.main(Test.java:12)

由于在a b c方法中我们都没有处理这个抛出的异常 , 一直到main方法里面抛出 , 于是异常在控制台中显示 , 程序执行终止 , 我们也无法看到success的字符串

关于方法栈, 也非常容易理解 , 参考下图调试器中的方法调用 , 执行的顺序为

main -> a -> b -> c

这里的每一条数据可以理解为栈帧

栈帧:一个栈帧随着一个方法的调用开始而创建,这个方法调用完成而销毁。栈帧内存放者方法中的局部变量,操作数栈等数据。

Java栈也称作虚拟机栈(Java Vitual Machine Stack),JVM栈只对栈帧进行存储,压栈和出栈操作。Java栈是Java方法执行的内存模型。

总结

慎用@Data , 使用@Setter @Getter等即可

别的方面 , 比如@Setter 方法的问题 , 变量aName 的set方法是 setAName , 这里显然不符合常见的命名规范 , 也可能会诱导别的问题

参考