1. 循环依赖的本质与三级缓存设计初衷
Spring框架中循环依赖问题一直是开发者面试和实际项目中的高频痛点。所谓循环依赖,简单来说就是Bean A依赖Bean B,同时Bean B又依赖Bean A,形成一个闭环。这种场景下,传统的依赖注入方式会导致无限递归,最终栈溢出。
Spring通过三级缓存机制巧妙解决了这个问题。三级缓存具体指:
- 第一级缓存(singletonObjects):存放完全初始化好的单例Bean
- 第二级缓存(earlySingletonObjects):存放早期暴露的Bean引用(已实例化但未完成属性注入)
- 第三级缓存(singletonFactories):存放Bean工厂对象,用于生成早期引用
关键理解:三级缓存的核心价值在于将Bean的实例化与初始化过程解耦,允许半成品Bean被提前暴露引用。这种"先上车后补票"的机制打破了循环依赖的死锁局面。
2. 典型误用场景与源码解析
2.1 @Async注解引发的缓存失效
一个常见的误用场景是在循环依赖的Bean上使用@Async注解。我们通过Debug模式跟踪AbstractAutowireCapableBeanFactory的doCreateBean方法:
java复制protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
// 实例化阶段
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
Object bean = instanceWrapper.getWrappedInstance();
// 将Bean工厂放入三级缓存(关键点)
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// 属性注入阶段可能触发循环依赖
populateBean(beanName, mbd, instanceWrapper);
// 初始化阶段(@Async代理在此生成)
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
问题根源在于:@Async代理是在初始化阶段生成的,而三级缓存保存的是原始对象。当另一个Bean注入该Bean时,拿到的是未被代理的原始对象,导致最终注入的Bean与容器中实际Bean不一致。
2.2 构造器注入的天然限制
Spring官方文档明确说明构造器注入方式不支持循环依赖。通过分析DefaultSingletonBeanRegistry的getSingleton方法:
java复制protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 先从一级缓存查
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 查二级缓存
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;
}
构造器注入时,Bean尚未实例化完成,连早期引用都无法提供,因此直接抛出BeanCurrentlyInCreationException。
3. Bean生命周期的完整闭环
3.1 标准生命周期流程图解
一个Bean的完整生命周期包含以下关键阶段:
- 实例化(Instantiation)
- 属性赋值(Population)
- 初始化(Initialization)
- Aware接口回调
- BeanPostProcessor前置处理
- InitializingBean.afterPropertiesSet
- 自定义init-method
- BeanPostProcessor后置处理
- 使用中(In Use)
- 销毁(Destruction)
在循环依赖场景下,生命周期会呈现交错执行的特点。例如Bean A的属性注入触发Bean B的创建,而Bean B又需要注入Bean A,此时Bean A可能处于刚完成实例化但未初始化的状态。
3.2 循环依赖下的异常生命周期案例
考虑以下配置类导致的特殊场景:
java复制@Configuration
public class ProblemConfig {
@Bean
public ServiceA serviceA() {
return new ServiceA(serviceB());
}
@Bean
public ServiceB serviceB() {
return new ServiceB(serviceA());
}
}
这种配置方式会绕过三级缓存机制,因为:
- 方法调用直接进入递归,不经过Spring的依赖查找流程
- 最终得到的两个Bean实例都不在Spring容器管理范围内
- 可能产生内存泄漏,因为容器外对象无法被正常销毁
4. 最佳实践与解决方案
4.1 设计层面的规避策略
- 应用分层架构:Controller -> Service -> Repository的经典分层能天然避免多数循环依赖
- 提取公共逻辑到新组件:将相互依赖的部分抽离为第三方服务
- 使用事件驱动模型:通过ApplicationEvent实现解耦
4.2 代码层面的解决方案
对于必须存在的循环依赖,推荐解决方案:
java复制// 方案1:使用setter注入 + @Lazy
@Service
public class ServiceA {
private ServiceB serviceB;
@Lazy
@Autowired
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
// 方案2:使用@Resource按名称注入
@Service
public class ServiceB {
@Resource(name = "serviceA")
private ServiceA serviceA;
}
4.3 高级场景处理技巧
对于需要AOP代理的场景,可以采用以下模式:
java复制@Service
public class ServiceA implements ApplicationContextAware {
private ApplicationContext context;
private ServiceB serviceBProxy; // 代理对象
@Autowired
private ServiceB serviceB; // 原始对象
@PostConstruct
public void init() {
// 手动获取代理对象
this.serviceBProxy = context.getBean(ServiceB.class);
}
@Override
public void setApplicationContext(ApplicationContext ctx) {
this.context = ctx;
}
}
这种方案虽然略显繁琐,但能保证在循环依赖场景下仍能获取到正确的代理对象。
5. 源码级调试技巧
5.1 关键断点设置建议
在IntelliJ IDEA中高效调试循环依赖问题:
- 在DefaultSingletonBeanRegistry的getSingleton方法设断点
- 在AbstractAutowireCapableBeanFactory的doCreateBean方法设断点
- 使用条件断点过滤特定Bean名称
5.2 诊断日志配置
在application.properties中添加:
properties复制logging.level.org.springframework.beans=DEBUG
logging.level.org.springframework.context=DEBUG
典型日志分析:
code复制Creating instance of bean 'serviceA'
Eagerly caching bean 'serviceA' to allow for resolving potential circular references
Finished creating instance of bean 'serviceA'
5.3 内存快照分析
当遇到复杂循环依赖时,可以使用Java内存分析工具:
- 使用jmap生成堆转储文件
- 通过MAT工具分析Bean的引用关系
- 重点关注重复创建的Bean实例
6. 新版Spring的变化与展望
Spring Framework 6.0对循环依赖处理做了优化:
- 更早的循环依赖检测
- 改进的错误提示信息
- 对GraalVM原生镜像的更好支持
一个值得注意的趋势是,随着响应式编程的普及,基于Project Reactor的编程模型天然避免了大多数循环依赖问题,因为其依赖关系是通过操作符链式调用的。
