本文来源:https://my.oschina.net/zhangxufeng/blog/3096394
在关于Spring的面试中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖的问题的。这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能够一下子思考出个中奥秘。本文主要针对这个问题,从源码的角度对其实现原理进行讲解。
- 过程演示
关于Spring bean的创建,其本质上还是一个对象的创建,既然是对象,读者朋友一定要明白一点就是,一个完整的对象包含两部分:当前对象实例化和对象属性的实例化。在Spring中,对象的实例化是通过反射实现的,而对象的属性则是在对象实例化之后通过一定的方式设置的。这个过程可以按照如下方式进行理解:
理解这一个点之后,对于循环依赖的理解就已经帮助一大步了,我们这里以两个类A和B为例进行讲解,如下是A和B的声明:
@Component
public class A {
private B b;
public void setB(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
public void setA(A a) {
this.a = a;
}
}
可以看到,这里A和B中各自都以对方为自己的全局属性。这里首先需要说明的一点是,Spring实例化bean是通过ApplicationContext.getBean()
方法来进行的。如果要获取的对象依赖了另一个对象,那么其首先会创建当前对象,然后通过递归的调用ApplicationContext.getBean()
方法来获取所依赖的对象,最后将获取到的对象注入到当前对象中。这里我们以上面的首先初始化A对象实例为例进行讲解。首先Spring尝试通过ApplicationContext.getBean()
方法获取A对象的实例,由于Spring容器中还没有A对象实例,因而其会创建一个A对象,然后发现其依赖了B对象,因而会尝试递归的通过ApplicationContext.getBean()
方法获取B对象的实例,但是Spring容器中此时也没有B对象的实例,因而其还是会先创建一个B对象的实例。读者需要注意这个时间点,此时A对象和B对象都已经创建了,并且保存在Spring容器中了,只不过A对象的属性b和B对象的属性a都还没有设置进去。在前面Spring创建B对象之后,Spring发现B对象依赖了属性A,因而此时还是会尝试递归的调用ApplicationContext.getBean()
方法获取A对象的实例,因为Spring中已经有一个A对象的实例,虽然只是半成品(其属性b还未初始化),但其也还是目标bean,因而会将该A对象的实例返回。此时,B对象的属性a就设置进去了,然后还是ApplicationContext.getBean()
方法递归的返回,也就是将B对象的实例返回,此时就会将该实例设置到A对象的属性b中。这个时候,注意A对象的属性b和B对象的属性a都已经设置了目标对象的实例了。读者朋友可能会比较疑惑的是,前面在为对象B设置属性a的时候,这个A类型属性还是个半成品。但是需要注意的是,这个A是一个引用,其本质上还是最开始就实例化的A对象。而在上面这个递归过程的最后,Spring将获取到的B对象实例设置到了A对象的属性b中了,这里的A对象其实和前面设置到实例B中的半成品A对象是同一个对象,其引用地址是同一个,这里为A对象的b属性设置了值,其实也就是为那个半成品的a属性设置了值。下面我们通过一个流程图来对这个过程进行讲解:
图中getBean()
表示调用Spring的ApplicationContext.getBean()
方法,而该方法中的参数,则表示我们要尝试获取的目标对象。图中的黑色箭头表示一开始的方法调用走向,走到最后,返回了Spring中缓存的A对象之后,表示递归调用返回了,此时使用绿色的箭头表示。从图中我们可以很清楚的看到,B对象的a属性是在第三步中注入的半成品A对象,而A对象的b属性是在第二步中注入的成品B对象,此时半成品的A对象也就变成了成品的A对象,因为其属性已经设置完成了。
- 源码讲解
对于Spring处理循环依赖问题的方式,我们这里通过上面的流程图其实很容易就可以理解,需要注意的一个点就是,Spring是如何标记开始生成的A对象是一个半成品,并且是如何保存A对象的。这里的标记工作Spring是使用ApplicationContext
的属性Set<String> singletonsCurrentlyInCreation
来保存的,而半成品的A对象则是通过Map<String, ObjectFactory<?>> singletonFactories
来保存的,这里的ObjectFactory
是一个工厂对象,可通过调用其getObject()
方法来获取目标对象。在AbstractBeanFactory.doGetBean()
方法中获取对象的方法如下:
这里的doGetBean()
方法是非常关键的一个方法(中间省略了其他代码),上面也主要有两个步骤,第一个步骤的getSingleton()
方法的作用是尝试从缓存中获取目标对象,如果没有获取到,则尝试获取半成品的目标对象;如果第一个步骤没有获取到目标对象的实例,那么就进入第二个步骤,第二个步骤的getSingleton()
方法的作用是尝试创建目标对象,并且为该对象注入其所依赖的属性。
这里其实就是主干逻辑,我们前面图中已经标明,在整个过程中会调用三次doGetBean()
方法,第一次调用的时候会尝试获取A对象实例,此时走的是第一个getSingleton()
方法,由于没有已经创建的A对象的成品或半成品,因而这里得到的是null,然后就会调用第二个getSingleton()
方法,创建A对象的实例,然后递归的调用doGetBean()
方法,尝试获取B对象的实例以注入到A对象中,此时由于Spring容器中也没有B对象的成品或半成品,因而还是会走到第二个getSingleton()
方法,在该方法中创建B对象的实例,创建完成之后,尝试获取其所依赖的A的实例作为其属性,因而还是会递归的调用doGetBean()
方法,此时需要注意的是,在前面由于已经有了一个半成品的A对象的实例,因而这个时候,再尝试获取A对象的实例的时候,会走第一个getSingleton()
方法,在该方法中会得到一个半成品的A对象的实例。然后将该实例返回,并且将其注入到B对象的属性a中,此时B对象实例化完成。然后将实例化完成的B对象递归的返回,此时就会将该实例注入到A对象中,这样就得到了一个成品的A对象。我们这里可以阅读上面的第一个getSingleton()
方法:
到这里,Spring整个解决循环依赖问题的实现思路已经比较清楚了。对于整体过程,读者朋友只要理解两点:
- Spring是通过递归的方式获取目标bean及其所依赖的bean的;
- Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。
结合这两点,也就是说,Spring在实例化一个bean的时候,是首先递归的实例化其所依赖的所有bean,直到某个bean没有依赖其他bean,此时就会将该实例返回,然后反递归的将获取到的bean设置为各个上层bean的属性的。
- 小结
本文首先通过图文的方式对Spring是如何解决循环依赖的问题进行了讲解,然后从源码的角度详细讲解了Spring是如何实现各个bean的装配工作的。
- 个人总结
Spring中循环依赖有2种情况:
1、构造器注入形成的循环依赖。也就是bean B需要在bean A的构造函数中完成初始化,bean A也需要在bean B的构造函数中完成初始化,这种情况的结果就是两个bean都无法完成初始化,循环依赖难以解决。
2、setter注入形成的循环依赖。bean A需要在bean B的setter()
方法中完成初始化,bean B也需要在bean A的setter()
方法中完成初始化,Spring设计的机制主要就是解决这种循环依赖,这也是上文阐述的内容。
再此处需要提及一下,Spring中Bean的作用域:
在默认情况下,Spring应用上下文中所有bean都是以单例singleton
的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多少次, 每次所注入的都是同一个实例。
在大多数情况下, 单例bean是很理想的方案。 初始化和垃圾回收对象实例所带来的成本只留给一些小规模任务, 在这些任务中, 让对象保持无状态并且在应用中反复重用这些对象可能并不合理。
有时候,可能会发现,你所使用的类是易变的mutable
,它们会保持一些状态,因此重用是不安全的。在这种情况下,将class声明为单例的bean就不再是好的方案了,因为对象可能会被改变, 稍后重用的时候会出现意想不到的问题。
Spring中定义了多种作用域, 可以基于这些作用域创建bean, 包括:
- 单例
Singleton
:在整个应用中, 只创建bean的一个实例。 - 原型
Prototype
: 每次注入或者通过Spring应用上下文获取的时候, 都会创建一个新的bean实例。 - 会话
Session
: 在Web应用中, 为每个会话创建一个bean实例。 - 请求
Request
: 在Web应用中, 为每个请求创建一个bean实例。
单例是默认的作用域, 但是正如之前所述, 对于易变的类型, 这并不合适。 如果选择其他的作用域, 要使用@Scope
注解, 它可以与@Component
或@Bean
一起使用。
例如, 如果你使用组件扫描来发现和声明bean, 那么你可以在bean的
类上使用@Scope
注解, 将其声明为原型bean:
@Component
@Scope{"prototype"}
public class Notepad{ }
当然你也可以使用@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
,但是使用SCOPE_PROTOTYPE
常量更加安全并不易出错。