手把手教你调试SpringBoot启动 IoC容器初始化源码,spring如何解决循环依赖

  • 授人以鱼不如授人以渔,首先声明这篇文章并没有过多的总结和结论,主要内容是教大家如何一步一步自己手动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提供的下图的回到上一个断点。再回到我们开始的地方,这样一步一步调试就不会迷路了。

 

 

 

 

 

 

  • 调试流程图:
    • 最后附上自己调试之后的总结流程图。下一篇博客,会结合这个流程图,解释一下,三级缓存的具体作用,以及为什么需要使用到了三级缓存?
posted @ 2020-12-10 14:53  coffeebabe  阅读(960)  评论(0编辑  收藏  举报