Spring如何实现循环依赖
1、spring如何初始化一个bean(跟循环依赖没有大的关系,作为知识普及)
普通java对象的实例化过程
普通的java对象是从一个程序员写好的java对象,经过编译形成class文件,然后在运行的时候,jvm遇到new关键字的时候,就会去方法区找到该类的模板,然后创造一个该类的对象,并分配到堆上。
javaBean的初始化过程
首先,java的类通过类加载器编程class文件,然后spring会把这个类的所有信息加载到一个BeanDefinition当中,其中的BeanClass属性是这个类的信息,BeanClassName是这个bean的名字。
然后spring会把所有BeanDefinition丢到一个map当中,在这个时候,我们能够通过实现BeanFactoryPostProcesser接口并且重写postProccessBeanFactory方法,拿到BeanFactory对象,通过这个工厂对象,调用getBeanDefinition方法就能够得到map中对应的beanDefinition(Spring默认Bean的名字等于Class的名字进行驼峰命名法,比如UserService -> userService),然后就可以对这个beanDefinition进行操作了。
最后,Spring的bean工厂会把BeanDefinition初始化成对应的bean。所以,如果把BeanDefinition修改了,比如修改beanClass,那么就可以进行“偷天换日”的操作!
简单示例代码如下:
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
BeanDefinition registerServiceBeanDefinition =
configurableListableBeanFactory.getBeanDefinition("registerService");
System.out.println(registerServiceBeanDefinition.getBeanClassName());
}
}
运行结果:
2、Spring的循环依赖(重头戏!)
不使用Spring
循环依赖顾名思义就是两个对象相互依赖对方,从而形成一个死锁。举个例子,
比如现在有两个类A和B:
public class A {
private B b = new B();
public A() {
System.out.println("Construct of A! ");
}
}
public class B {
private A a = new A();
public B() {
System.out.println("Construct of B!");
}
}
可见,类A依赖于B,当我们实例化A时,A必须先去实例化B,而B又依赖于A,又要实例化A,如此循环往复,永远达不到终点,相当于一个死锁。
我们来运行一下测试类:
@Test
public void test() throws Exception {
A a = new A();
}
运行结果:
可以看到,由于A和B的循环依赖造成了死循环,最终方法栈溢出,程序崩溃!
使用Spring
这一次,我们使用Spring来进行循环依赖,代码如下:
@Component
public class A {
@Autowired
private B b;
public A() {
System.out.println("Construct of A! ");
}
}
@Component
public class B {
@Autowired
private A a;
public B() {
System.out.println("Construct of B!");
}
}
我们来运行一下测试类:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class ApplicationTest {
@Autowired
A a;
@Test
public void test() throws Exception {
}
}
运行结果:
可以看到,使用了Spring的依赖注入,我们实现了循环依赖,类A和B都初始化成功了!那么,Spring是如何来完成这个看似实现不了的功能的呢?
Spring实现循环依赖原理
Spring注入一个Bean的过程
作为前提,这里先介绍一下Spring注入一个Bean的过程。
1、Spring会扫描到这个类需要被注入
2、Spring会判断这个类是否已经在容器的单例池中,如果已经存在,则会直接从单例池中拿到这个Bean,注入完成。
3、如果单例池没有这个Bean,Spring则会判断这个Bean是否正在被创建,如果正在被创建,则说明已经循环依赖了!这时候会判断Spring是否开启了支持循环依赖,如果开启了就把正在被创建的这个实例注入。如果没有开启则会抛出异常。
3、如果单例池中没有这个Bean,Spring会创建一个类的实例,这时候就会调用构造方法,这个实例是一个“半成品”,还没有注入其他的依赖。注意,这里有一个很重要的操作,就是把这个实例放入一个二级缓存Map中(一级缓存是单例池),表示这个类正在被创建。
4、扫描类中的依赖,然后注入这些依赖。
5、走完Bean生命周期的其他步骤,注入成功。
举例子分析
拿上述的类A和类B相互依赖来说,在Spring应用开启之后,Spring会扫描需要注入的类,并生成对应的BeanDefinition,最终生成对应的Bean放入单例池中。现在假如容器扫描到了类A需要被注入。
1、容器会先判断A是否已经在单例池中,由于我们之前没有注入过A,所以A不在单例池中,那么Spring会创建一个A的实例。并通知Spring类A正在创建中。
2、容器会扫描类A依赖了哪些其他类,现在扫描到了A依赖于B。
3、创建类B的实例,通知Spring类B正在创建中。
4、扫描类B的依赖,扫描到了需要依赖类A。
5、判断到类A正在创建中,所以直接拿到类A的实例注入进来。
6、层层返回,类A注入成功!
总结
循环依赖通俗来讲也就是“无限套娃”,如果不加以控制,就会造成程序的栈溢出。那么,为了解决循环依赖问题,Spring提供了一套解决方案:在创建对象A时,通知容器此对象正在被创建,那么,当循环引用时,容器需要注入A时,就会扫描到A正在被创建,然后直接把A的半成品注入,从而解决循环依赖的问题。简单的说,就是A->B->A的半成品。
最后,如果想要进一步了解Spring源码怎么实现循环依赖的,参考此文章:
阿里P8级别的spring源码关于循环引用的笔记