前记:千万不要在实际项目开发中这么玩。
在Spring中,如果A类和B类之间存在相互依赖(即A类有一个B类的属性,B类有一个A类的属性),这种情况称为循环依赖(Circular Dependency)。Spring框架对循环依赖的处理机制较为复杂,但通过其内部的缓存机制可以解决部分循环依赖问题。以下是具体分析:
三级缓存机制
Spring如何处理循环依赖?
Spring通过三级缓存机制来处理单例Bean的循环依赖问题,具体分为以下步骤:
1. 实例化阶段(Instantiation)
- 当Spring需要创建A类的Bean时,会先实例化A(通过反射调用无参构造函数),此时A的属性(包括B类的属性)尚未注入。
- Spring会将未完全初始化的A的实例放入**一级缓存(singletonObjects)**中。
2. 依赖注入阶段(Dependency Injection)
- 在为A注入B类属性时,Spring发现B还未创建,于是转而创建B的Bean。
- 创建B时,Spring会先实例化B(调用无参构造函数),此时B的属性(包括A类的属性)尚未注入。
- Spring将未完全初始化的B的实例放入一级缓存中。
- 接着,Spring尝试为B注入A类的属性,此时发现A已经在一级缓存中(虽然A的B属性还未注入完毕)。
- 此时,Spring会从一级缓存中取出A的半成品实例(此时A的B属性尚未完成注入),并将其注入到B的A属性中。
3. 继续注入A的B属性
- 回到A的依赖注入阶段,Spring现在可以为A注入B的实例(因为B已经实例化完成)。
- 此时A的B属性被成功注入,但B的A属性已经提前注入了A的半成品实例。
4. 初始化阶段(Initialization)
- 当所有依赖注入完成后,Spring会依次调用A和B的初始化方法(如
@PostConstruct
、init-method
或InitializingBean.afterPropertiesSet()
)。 - 这里需要注意:A和B的初始化方法可能会在对方的属性尚未完全初始化时被调用,因此需要确保代码逻辑能够处理这种情况。
关键点总结
-
支持的循环依赖类型:
- Setter注入或字段注入:Spring可以通过三级缓存机制处理这种循环依赖。
- 构造器注入:Spring无法处理构造器注入的循环依赖,会直接抛出
BeanCurrentlyInCreationException
异常。
-
处理流程:
- 一级缓存(singletonObjects):存储完全初始化的Bean。
- 二级缓存(earlySingletonObjects):存储早期暴露的Bean实例(用于处理循环依赖)。
- 三级缓存(singletonFactories):存储ObjectFactory对象,用于延迟获取Bean。
-
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { /** 一级缓存,存储完成初始化的对象 Cache of singleton objects: bean name to bean instance. */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); /** 三级缓存:Cache of singleton factories: bean name to ObjectFactory. */ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); /** 二级缓存,存储提前暴露出来的对象 Cache of early singleton objects: bean name to bean instance. */ private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
-
面试官可能会问,二级缓存已经够了,为啥Spring提供了三级缓存呢?
-
因为咱们Spring提供了AOP的机制,如果某个bean需要被代理,需要将代理对象提前暴露出来,不能对外暴露未代理的对象。
-
而Spring提供的三级缓存,他存储的ObjectFactory类型,他是一个函数式接口,三级缓存中本质存储的是一个Lambda表达式,需要获取对应的对象时,需要调用这个ObjectFactory中的getObject方法才能获取。
-
这样如果对象需要被代理,就可以基于三级缓存中提供的getObject的方式将对象代理后,再从三级缓存中拿到二级或者一级缓存。
-
-
如果Spring没有AOP的这个机制需要处理,那其实二级缓存已经足够了。 But,Spring有代理的操作,所以他需要这个三级缓存,来将bean的代理对象构建出来返回。
-
-
潜在风险:
- 如果在Bean的初始化方法(如
@PostConstruct
)或业务方法中,访问对方Bean的属性,可能会得到未完全初始化的值,导致逻辑错误。 - 循环依赖会增加代码的复杂性,降低可维护性,应尽量避免。
- 如果在Bean的初始化方法(如
如何避免循环依赖?
-
重构代码:
- 将共同依赖提取到第三个类中,打破循环。
- 通过接口解耦,减少直接依赖。
-
使用Setter注入而非构造器注入:
- Spring仅支持Setter或字段注入的循环依赖,构造器注入会导致失败。
-
引入中间Bean:
- 通过引入中间类作为中介,分离A和B的直接依赖。
示例代码
假设A和B的依赖关系如下:
@Component
public class A {
@Autowired
private B b; // A依赖B
}
@Component
public class B {
@Autowired
private A a; // B依赖A
}
Spring的处理流程:
- 创建A的实例(未注入B)。
- 将A放入一级缓存。
- 创建B的实例(未注入A)。
- 将B放入一级缓存。
- 为B注入A时,从一级缓存获取A的半成品实例。
- 为A注入B的完整实例。
- 依次调用A和B的初始化方法。
总结
Spring通过缓存机制可以处理Setter或字段注入的循环依赖,但存在潜在风险。建议在设计时尽量避免循环依赖,通过合理拆分代码或引入中间层来消除依赖循环,从而提升代码的可维护性和稳定性。
后记:绝对不能在实际开发项目中这么玩啊,否则被你老大发现就得挨削了。
(望各位潘安、各位子健/各位彦祖、于晏不吝赐教!多多指正!🙏)