- 授人以鱼不如授人以渔,首先声明这篇文章并没有过多的总结和结论,主要内容是教大家如何一步一步自己手动debug调试源码,然后总结spring如何解决的循环依赖,最后,操作很简单,有手就行。
- 本次调试 是使用@Autowired注入,通过来调试源码看spring如何解决的循环依赖问题。
- 首先创建一个简单的springBoot项目,引入spring-boot-test包即可。可以使用idea提供的spring官网推荐的快速创建。
-
maven依赖
-
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency> -
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency> -
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
</dependency> -
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.1</version>
<scope>compile</scope>
</dependency> -
编写调试源码的三个类
-
@Getter
@Setter
@Component
public class A {
@Autowired
private B b;
} -
@Getter
@Setter
@Component
public class B {
@Autowired
private A a;
} -
@SpringBootTest
@RunWith(SpringRunner.class)
public class IocCodeSourceTest {
@Autowired
private ApplicationContext applicationContext;
@Test
public void test_sourceCode_01() {
Object bean = applicationContext.getBean("a");
Object bean1 = applicationContext.getBean("b");
//通过在此打断点设置可以发现,A和B都初始化完成了
System.out.println(bean);
System.out.println(bean1);
}
} - 注意:这里不能使用lombok提供的注解@Data,因为@Data中重写了equals和hascodes方法,
System.out.println(bean)
这一句代码这输出的时候,会去调用bean的hashcode,在计算A对象的hashcode的时候,又调用了属性B的hashcode方法,同理计算B属性的hashcode由去调用了A的,造成了死递归。最终造成线程的虚拟机栈(方法栈)一直入栈,栈溢出。如下图,hashcode9331和9332一直会递归调用。 - 没时间的童鞋可以直接去看我的github项目地址:https://github.com/coffeebabeCGG/spring/tree/master/src/main/java/com/example/springstudy
-
- 调试方法:
首先需要知道,spring默认创建bean都是单例的,而单例的bean是在容器初始化完成后就创建完成。因此这里使用springboot启动调试,最终还是会进入到run方法,到refresh()方法,再去初始化容器。
下面会教几个关键的debug调试点。掌握这几个关键的调试点后,源码怎么走的,可以看最终的调试结果流程图,自己结合源码去一步一步debug。
1、进入getbean()
2、紧接着进去AbstractBeanFactory实现方法
3、最后进入AbstractBeanFactory的doGetBean()
4、关键点:在debug上设置条件,因为容器初始化的时候,会有很多spring本身的bean需要先去加载,所以,要在这里设置我们需要去debug观察的bean,我们的bean没有配置beanName,默认使用的类名小写。
5、接下来就可以一路F9放行,小伙伴们看自己的快捷键,每个人都不一样。当断点停下,就可以观察spring在创建 A B对象的调用栈了。
关键点:调试源码的时候,深入之后很容易就迷路,有个小技巧。下面截图的左下角红色标注,小伙伴们一定记得学会看方法的调用栈。为了简单理解,可以先这么理解,在开始调用一个方法时候,Jvm会创建一个线程,例如下图就是main线程。
然后 方法之间的调用是通过栈 这个数据结构来实现的。Jvm中称为java虚拟机栈,每一个方法都是一个栈帧。
结合下图去理解:
main线程中(即一个虚拟机栈):run方法是一个栈帧,然后run方法中调用了refreshContxt,那么refreshContext也是一个栈帧,被压入栈顶,以此类推。目前我们当前main线程的虚拟机栈的栈顶是doGetBean方法。
说了这么多有什么用呢?加入我们在调试过程中,迷路了,我们可以看下调用栈,找到我们来的地方。是哪一个栈帧,可以利用idea提供的下图的回到上一个断点。再回到我们开始的地方,这样一步一步调试就不会迷路了。
- 调试流程图:
- 最后附上自己调试之后的总结流程图。下一篇博客,会结合这个流程图,解释一下,三级缓存的具体作用,以及为什么需要使用到了三级缓存?