2025-06-18
Spring
0

目录

Spring循环依赖的奥秘:三级缓存的设计与局限性
引言
什么是循环依赖
Spring的三级缓存机制
为什么需要三级缓存而不是二级
解决循环依赖的流程
Spring无法解决的循环依赖场景
1. 构造器注入的循环依赖
2. Prototype作用域的循环依赖
3. @Async方法的循环依赖
4. @Transactional方法的循环依赖
最佳实践
总结

Spring循环依赖的奥秘:三级缓存的设计与局限性

引言

在Spring框架的日常使用中,循环依赖是一个常见但又令人困惑的话题。为什么Spring要使用三级缓存来解决循环依赖?什么样的循环依赖Spring又无法解决?本文将深入探讨这些问题,帮助开发者更好地理解Spring的底层机制。

什么是循环依赖

循环依赖指的是两个或多个Bean相互依赖,形成一个闭环。例如:

java
@Service public class A { @Autowired private B b; } @Service public class B { @Autowired private A a; }

这种情况下,A依赖B,B又依赖A,就形成了循环依赖。

Spring的三级缓存机制

Spring通过三级缓存巧妙地解决了大多数循环依赖问题。让我们先了解这三级缓存:

  1. 一级缓存(singletonObjects):存放完全初始化好的Bean
  2. 二级缓存(earlySingletonObjects):存放原始Bean对象(尚未填充属性)
  3. 三级缓存(singletonFactories):存放Bean工厂对象,用于生成原始Bean

为什么需要三级缓存而不是二级

很多开发者会问:为什么不能只用二级缓存?关键在于AOP代理

  1. 代理对象的生成时机:如果Bean需要被AOP代理,代理对象应该在原始对象创建后立即生成,而不是在属性填充和初始化之后。

  2. 三级缓存的作用:三级缓存中的ObjectFactory可以在需要时决定是否返回代理对象。如果是普通Bean,直接返回原始对象;如果需要代理,则返回代理对象。

  3. 二级缓存的局限性:如果只有二级缓存,我们无法区分是应该返回原始对象还是代理对象。三级缓存通过工厂模式提供了这种灵活性。

解决循环依赖的流程

以A和B的循环依赖为例:

  1. 开始创建A,实例化后放入三级缓存
  2. 填充A的属性时发现需要B
  3. 开始创建B,实例化后放入三级缓存
  4. 填充B的属性时发现需要A
  5. 从三级缓存获取A的工厂,获取早期引用(可能是代理对象)
  6. B完成创建,放入一级缓存
  7. A得到B的引用,完成创建,放入一级缓存

Spring无法解决的循环依赖场景

尽管三级缓存很强大,但以下情况的循环依赖Spring无法解决:

1. 构造器注入的循环依赖

java
@Service public class A { private final B b; public A(B b) { this.b = b; } } @Service public class B { private final A a; public B(A a) { this.a = a; } }

原因:构造器注入需要在实例化阶段就完成依赖注入,此时Bean还未创建,无法放入缓存。

2. Prototype作用域的循环依赖

java
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Service public class A { @Autowired private B b; } @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Service public class B { @Autowired private A a; }

原因:Prototype作用域的Bean不会缓存,每次请求都创建新实例,无法通过缓存解决循环依赖。

3. @Async方法的循环依赖

java
@Service public class A { @Autowired private B b; @Async public void asyncMethod() {} } @Service public class B { @Autowired private A a; }

原因:@Async方法需要代理,而代理的创建时机可能导致循环依赖无法解决。

4. @Transactional方法的循环依赖

类似于@Async,某些事务代理场景也可能导致循环依赖问题。

最佳实践

  1. 尽量避免循环依赖:良好的设计应该避免循环依赖,考虑重构为单向依赖
  2. 优先使用setter注入:如果必须循环依赖,使用setter注入而非构造器注入
  3. 使用@Lazy:在某些场景下,可以使用@Lazy注解延迟加载依赖
java
@Service public class A { @Lazy @Autowired private B b; }

总结

Spring的三级缓存设计是一个精妙的解决方案,它通过区分不同阶段的Bean引用,巧妙地处理了大多数循环依赖场景。然而,理解其局限性同样重要,特别是构造器注入和某些代理场景下的循环依赖问题。作为开发者,我们应当首先考虑设计层面的解耦,其次才是依赖框架提供的解决方案。

希望本文能帮助你更好地理解Spring的循环依赖处理机制,在实际开发中做出更合理的设计决策。