在Spring框架中,循环依赖问题是指两个或多个bean相互依赖,形成一个循环引用。这个问题在使用依赖注入时比较常见。理解如何识别和解决这个问题对于开发者来说很重要。
什么是循环依赖?
循环依赖简单来说就是A依赖B,而B又依赖A。例如:
java
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
在上面的例子中,A
依赖于B
,同时B
也依赖于A
,这就形成了一个循环依赖。
解决循环依赖的方法
-
Setter注入(Setter Injection) : 使用setter方法进行依赖注入,可以有效地解决大部分循环依赖问题。
Spring先实例化bean,然后再进行属性填充,这样可以打破循环依赖
。java@Component public class A { private B b; @Autowired public void setB(B b) { this.b = b; } } @Component public class B { private A a; @Autowired public void setA(A a) { this.a = a; } }
-
@Lazy注解 : 使用
@Lazy
注解延迟加载bean,使得Spring在实际需要bean的时候才进行初始化,从而避免循环依赖。java@Component public class A { private final B b; @Autowired public A(@Lazy B b) { this.b = b; } } @Component public class B { private final A a; @Autowired public B(@Lazy A a) { this.a = a; } }
-
构造器注入(Constructor Injection) :
如果使用构造器注入,则无法直接解决循环依赖问题,因为Spring在创建bean时需要先解析构造函数的参数,存在循环依赖的话,无法完成实例化
。这种情况下,需要重新设计类之间的关系,或者使用其他方式,如工厂模式来间接解决。 -
ApplicationContextAware接口 : 通过实现
ApplicationContextAware
接口,可以手动从Spring上下文中获取依赖bean。这种做法虽然能解决问题,但会增加代码复杂度,不推荐广泛使用。java@Component public class A implements ApplicationContextAware { private B b; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.b = applicationContext.getBean(B.class); } } @Component public class B { private final A a; @Autowired public B(A a) { this.a = a; } }
-
重新设计: 如果代码中出现循环依赖问题,说明代码结构设计不合理,没有做好分层工作;架构设计应该遵循:上层依赖下层,下层不应该反向依赖上层;如果存在共用部分,应该放到抽象层,上下层共同依赖抽象层。
三级缓存机制
Spring主要通过三级缓存机制来解决循环依赖问题。在Spring的DefaultSingletonBeanRegistry
类中,维护了三个缓存:
- singletonObjects:一级缓存,用于存放完全初始化好的单例bean。
- earlySingletonObjects:二级缓存,用于存放那些实例化完成但未初始化完成的早期单例bean。
- singletonFactories:三级缓存,用于存放能够创建早期单例bean的工厂。
三级缓存singletonFactories的作用
三级缓存的核心思想是,尽早暴露对象的引用,以便其他bean可以依赖这些尚未完全初始化的bean,从而解决循环依赖的问题。
源码分析
以下是Spring解决循环依赖的关键步骤:
-
实例化Bean: 当Spring创建一个bean时,会先检查这个bean是否已经被创建。如果没有,则实例化该bean(调用构造函数)。
javaprotected Object createBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) { //...省略部分代码 try { // 第一步:实例化Bean instanceWrapper = createBeanInstance(beanName, mbd, args); bean = instanceWrapper.getWrappedInstance(); beanType = instanceWrapper.getWrappedClass(); // 判断是否允许提前暴露 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } } //...省略部分代码 }
-
提前暴露Bean引用 : Spring会将实例化后的bean包装成一个
ObjectFactory
放入三级缓存singletonFactories
中。这样,如果另一个bean在依赖注入过程中需要这个bean,可以通过ObjectFactory
获取到早期引用。javaprotected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } }
-
属性填充和初始化: 接下来,Spring会进行属性填充和初始化。这时候,如果有其他bean依赖当前bean,就会通过三级缓存获取早期引用。
javaprotected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException { //...省略部分代码 populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); //...省略部分代码 }
-
从缓存中获取Bean : 如果在依赖注入过程中发现需要一个尚未完全初始化的bean,Spring会首先从一级缓存
singletonObjects
中查找,如果找不到,再从二级缓存earlySingletonObjects
中查找,最后从三级缓存singletonFactories
中查找并通过工厂方法创建早期引用。java@Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }
总结
通过三级缓存机制,Spring能够缓解因为循环依赖导致的bean创建失败问题。在bean实例化后,Spring会将其放入三级缓存中的工厂里,从而使得其他依赖它的bean能够获取到其早期引用。随后在属性填充和初始化阶段,Spring会逐步将bean移动到二级缓存和一级缓存中,从而最终解决循环依赖问题。
示例说明
java
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
当Spring IOC容器初始化上述两个bean时,三级缓存机制会按如下步骤工作:
-
创建A:
A
正在创建中,将其标记并放入三级缓存。
-
注入B到A:
- Spring发现需要注入
B
,于是开始创建B
。
- Spring发现需要注入
-
创建B:
B
正在创建中,将其标记并放入三级缓存。
-
注入A到B:
- 由于
A
正在创建中,Spring会从三级缓存中获取A
的ObjectFactory,通过该工厂返回一个早期暴露的A
(代理对象或部分初始化的对象),并将其放入二级缓存。
- 由于
-
完成B的创建并将其放入一级缓存:
- 注入完成后,
B
的创建过程结束,将其放入一级缓存。
- 注入完成后,
-
完成A的创建并将其放入一级缓存:
- 最终,
A
的创建过程结束,将其从三级缓存移出并放入一级缓存。
- 最终,
通过这种机制,Spring可以在处理构造器循环依赖时成功初始化所有bean而不引发无限递归。