地面部队免安装绿色版
49.3G · 2025-10-26
循环依赖指的是当两个或多个Spring的bean相互依赖,而这些bean都需要被实例化来满足依赖,导致Spring容器无法顺利完成Bean的初始化过程。具体来说,A依赖B,B又依赖A,导致一个死循环。Spring必须处理这种依赖关系,避免造成死锁或失败。
Spring容器如何解决这个问题呢?Spring采用了两种策略来解决循环依赖问题:
在 Spring 中,Bean 的创建分为三个阶段:实例化、属性注入、初始化。
三级缓存的目的是通过延迟暴露“半成品”对象,打破循环依赖。
假设有 A 依赖 B,B 又依赖 A 的场景:
实例化 Bean A
处理 A 的依赖:注入 B
处理 B 的依赖:注入 A
B 初始化完成
A 初始化完成
1. 实例化 A,放入三级缓存(工厂)。
2. A -> 依赖 B,开始创建 B。
3. 实例化 B,放入三级缓存(工厂)。
4. B -> 依赖 A,从三级缓存生成“半成品 A”。
5. 完成 B 的初始化,放入一级缓存。
6. A 注入完整 B,完成初始化,放入一级缓存。
以下是 Spring 内部处理缓存的简化逻辑:
// 获取单例 Bean 的方法
public Object getSingleton(String beanName) {
// 1. 从一级缓存获取完整的 Bean
Object singleton = singletonObjects.get(beanName);
if (singleton == null) {
// 2. 从二级缓存获取“半成品” Bean
singleton = earlySingletonObjects.get(beanName);
if (singleton == null) {
// 3. 从三级缓存获取工厂并生成 Bean
ObjectFactory<?> factory = singletonFactories.get(beanName);
if (factory != null) {
singleton = factory.getObject();
earlySingletonObjects.put(beanName, singleton);
singletonFactories.remove(beanName);
}
}
}
return singleton;
}
因为原型(Prototype)模式每次都创建新对象,不能复用“半成品”。而单例对象可以复用同一个“半成品”,这才是循环依赖能解决的关键!
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject;
}Copy to clipboardErrorCopied
在这种情况下,理论上二级缓存是够用的。
但Spring的设计是统一的,不能因为某些场景简单就采用不同的架构。
假设A需要被AOP代理(比如有@Transactional注解)。
问题在于:代理对象生成的时机。
Spring AOP代理对象的创建最佳时机是在Bean的初始化之后(在BeanPostProcessor的后置处理中)。但如果存在循环依赖,B在创建过程中就需要A,而此时A还没有走到初始化那一步。
如果只有二级缓存:
这就导致了严重的不一致问题:
这违背了单例的原则!同一个Bean(A)在容器中有了两个不同的实例(一个原始对象在B里,一个代理对象在容器里),这会导致事务等AOP功能在B调用A时完全失效,是一个致命的Bug。
三级缓存的精髓在于:我不提前给你对象,我给你一个“造物工厂”
流程如下:
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
getSingleton(A)。Spring发现A在三级缓存中有ObjectFactory → 调用它,执行getEarlyBeanReference()。
此时判断A是否需要代理:
将结果放入二级缓存,并从三级缓存移除。
B拿到A的引用。
A继续创建,完成后放入一级缓存中,并清理二级缓存中的早期引用 。
@Lazy 注解可以让 Spring 稍微等一等,延迟对象的创建。它的作用就是让 Spring 不急着立刻创建对象,而是等到真正需要的时候才去创建。
当你在一个对象上使用 @Lazy,Spring 会选择不立刻创建它,而是等到这部分真正被调用的时候才去创建。这样,Spring 就能够打破这两个对象相互等待的死循环,最终成功地创建这两个对象。
虽然 @Lazy 注解能帮你解决循环依赖问题,但实际上,还是建议尽量避免循环依赖。因为如果经常出现这种情况,代码的逻辑会变得 复杂,也不容易维护。所以,最好从设计上尽量避免这种问题。
@Lazy 是 Spring 提供的一种延迟加载机制,意思是“等到用我的时候再初始化,不着急”。这就像告诉 Spring:“别急着建这个 Bean,等真需要再搞定。”
假设我们有两个类 A 和 B,它们通过构造器互相依赖:
@Component
public class A {
private final B b;
@Autowired
public ClassA(@Lazy B b) { // 使用 @Lazy,让 classB 延迟初始化
this.b = b;
}
}
@Component
public class B {
private final A a;
@Autowired
public B(A a) {
this.a = a;
}
}
关键点:
A 创建时,Spring 不会急着实例化 B。A 真正需要用到 B 时,Spring 才去初始化 B。虽然 @Lazy 方便,但不能滥用!过度依赖延迟加载,可能会带来以下问题:
类级别延迟: Bean 初始化比较耗时时,使用 @Lazy 提升系统启动速度。
@Component
@Lazy
public class HeavyBean {
public HeavyBean() {
System.out.println("HeavyBean 初始化了!");
}
}
注入延迟: 某些依赖加载会导致性能瓶颈,可以使用 @Lazy 注解。
@Component
public class SomeService {
private final HeavyBean heavyBean;
@Autowired
public SomeService(@Lazy HeavyBean heavyBean) {
this.heavyBean = heavyBean;
}
}
@Lazy 是解决构造器循环依赖的好工具,但别过度使用。