Spring循环依赖详解
Spring循环依赖详解
什么是循环依赖?
循环依赖是指两个或多个Bean之间相互依赖,形成一个循环引用的情况。例如:
@Service
public class A {@Autowiredprivate B b;
}@Service
public class B {@Autowiredprivate A a;
}
在上面的例子中,Bean A依赖Bean B,同时Bean B也依赖Bean A,这就形成了循环依赖。
Spring Bean的生命周期
在深入理解循环依赖之前,我们需要先了解Spring Bean的完整生命周期:
-
实例化(Instantiation)
- 调用构造函数创建对象
- 此时对象还未完成初始化
- 对象处于"半成品"状态
-
属性赋值(Populate Properties)
- 设置对象属性值
- 注入依赖的其他Bean
- 执行@Autowired等注解的注入
-
初始化(Initialization)
- 调用@PostConstruct注解的方法
- 实现InitializingBean接口的afterPropertiesSet方法
- 执行自定义的init-method方法
- 执行BeanPostProcessor的postProcessAfterInitialization方法
-
使用(In Use)
- Bean可以被应用程序使用
- 所有属性都已正确初始化
- 所有初始化方法都已执行
-
销毁(Destruction)
- 调用@PreDestroy注解的方法
- 实现DisposableBean接口的destroy方法
- 执行自定义的destroy-method方法
Spring如何解决循环依赖?
关键就是提前暴露未完全创建完毕的 Bean。
在 Spring 中主要是使用三级缓存来解决了循环依赖:
- 一级缓存(Singleton Objects Map):用于存储完全初始化完成的单例Bean。
- 二级缓存(Early Singleton Objects Map):用于存储尚未完全初始化,但已实例化的Bean,用于提前暴露对象,避免循环依赖问题。
- 三级缓存(Singleton Factories Map):用于存储对象工厂,当需要时,可以通过工厂创建早期Bean(特别是为了支持AOP代理对象的创建)。
解决步骤
- Spring 首先创建 Bean 实例,并将其加入三级缓存中(Factory)。
- 当一个 Bean 依赖另一个未初始化的 Bean 时,Spring 会从三级缓存中获取 Bean 的工厂,并生成该 Bean 的对象(若有代理则是代理对象)。
- 代理对象存入二级缓存,解决循环依赖。
- 一旦所有依赖 Bean 被完全初始化,Bean 将转移到一级缓存中。
源码分析
Spring的核心实现在DefaultSingletonBeanRegistry
类中:
public class DefaultSingletonBeanRegistry extends FactoryBeanRegistrySupport {// 一级缓存:完全初始化好的Beanprivate final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);// 二级缓存:提前曝光的Bean(未完成属性填充)private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);// 三级缓存:Bean的工厂对象private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);// 正在创建的Bean名称集合private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}
三级缓存详解
缓存的作用和特点
-
一级缓存(singletonObjects)
- 存储完全初始化好的Bean
- 可以直接被使用
- 线程安全(ConcurrentHashMap)
- 最终存储的Bean实例
- 包含所有初始化完成的属性
- 是最终可用的Bean
-
二级缓存(earlySingletonObjects)
- 存储提前曝光的Bean
- 用于解决普通Bean的循环依赖
- 避免重复创建对象
- 存储的是原始对象或代理对象
- 生命周期较短,主要用于解决循环依赖
- 是"半成品"Bean
-
三级缓存(singletonFactories)
- 存储Bean的工厂对象(ObjectFactory)
- 用于处理AOP代理对象的循环依赖
- 可以返回代理对象或原始对象
- 工厂对象可以动态决定返回什么类型的对象
- 支持AOP的动态代理
- 是创建Bean的工厂
为什么需要三级缓存?
三级缓存的存在主要是为了解决AOP代理对象的循环依赖问题。具体原因如下:
-
AOP代理的特殊性
- AOP代理对象需要在Bean初始化完成后才能创建
- 代理对象需要包装原始对象
- 代理对象的创建时机与普通Bean不同
- 代理对象需要等待所有BeanPostProcessor执行完成
-
获取Bean的早期引用
/*** 获取Bean的早期引用,用于解决循环依赖* @param beanName Bean的名称* @param mbd Bean的定义信息* @param bean 原始Bean对象* @return 可能是原始Bean或代理对象*/ protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {// 默认使用原始Bean对象Object exposedObject = bean;// 判断是否需要创建代理对象// 1. mbd.isSynthetic()为false,表示不是合成的Bean// 2. 存在InstantiationAwareBeanPostProcessor处理器if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {// 遍历所有的BeanPostProcessorfor (BeanPostProcessor bp : getBeanPostProcessors()) {// 只处理SmartInstantiationAwareBeanPostProcessor类型的处理器if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;// 调用处理器的getEarlyBeanReference方法// 这个方法可能会返回代理对象,也可能返回原始对象exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);}}}// 返回处理后的对象(可能是原始对象或代理对象)return exposedObject; }
-
三级缓存的必要性
- 一级缓存:存储最终可用的Bean
- 二级缓存:存储提前曝光的Bean
- 三级缓存:存储Bean的工厂对象,用于处理AOP代理
- 三级缓存可以动态决定是否创建代理对象
详细的解决流程
-
Bean A的创建过程
- 首先从一级缓存中查找Bean A
- 如果不存在,标记Bean A正在创建中(singletonsCurrentlyInCreation)
- 实例化Bean A(调用构造函数)
- 将Bean A的工厂对象放入三级缓存
- 开始填充Bean A的属性
-
Bean B的创建过程
- 发现Bean A依赖Bean B
- 从一级缓存中查找Bean B
- 如果不存在,开始创建Bean B
- 实例化Bean B
- 将Bean B的工厂对象放入三级缓存
- 开始填充Bean B的属性
-
循环依赖的解决
- 发现Bean B依赖Bean A
- 从一级缓存中查找Bean A(不存在)
- 从二级缓存中查找Bean A(不存在)
- 从三级缓存中获取Bean A的工厂对象
- 通过工厂对象获取Bean A(可能是代理对象)
- 将获取到的Bean A放入二级缓存
- 完成Bean B的属性填充
- 将Bean B放入一级缓存
- 完成Bean A的属性填充
- 将Bean A放入一级缓存
注意事项
-
单例Bean的限制
- 只有单例Bean才能解决循环依赖
- 原型Bean每次都会创建新实例,无法解决循环依赖
- 构造器注入的循环依赖无法解决
- 非单例Bean的循环依赖会导致无限递归
-
AOP相关
- 需要开启AOP功能才能使用三级缓存
- 代理对象的创建时机影响循环依赖的解决
- 某些AOP场景可能无法解决循环依赖
- 代理对象的完整性依赖于原始对象
-
性能考虑
- 三级缓存机制会增加内存使用
- 循环依赖会影响Bean的初始化性能
- 建议避免使用循环依赖
- 过多的循环依赖会导致应用启动变慢
最佳实践
-
架构设计
- 尽量避免循环依赖
- 使用事件机制解耦
- 考虑使用观察者模式
- 合理划分模块职责
- 使用接口隔离原则
-
依赖注入方式
- 优先使用构造器注入
- 如果必须使用循环依赖,使用setter注入
- 避免使用字段注入(@Autowired)
- 使用@Lazy注解延迟加载
-
代码示例
// 推荐的方式:使用构造器注入 @Service public class ServiceA {private final ServiceB serviceB;@Autowiredpublic ServiceA(ServiceB serviceB) {this.serviceB = serviceB;} }// 如果必须使用循环依赖,使用setter注入 @Service public class ServiceA {private ServiceB serviceB;@Autowiredpublic void setServiceB(ServiceB serviceB) {this.serviceB = serviceB;} }// 使用@Lazy注解延迟加载 @Service public class ServiceA {@Autowired@Lazyprivate ServiceB serviceB; }
常见问题
-
为什么构造器注入的循环依赖无法解决?
- 构造器注入发生在Bean实例化阶段
- 此时Bean还未创建完成
- 无法提前暴露Bean实例
- 会导致无限递归
-
为什么原型Bean的循环依赖无法解决?
- 原型Bean每次都会创建新实例
- 无法使用缓存机制
- 会导致无限递归
- 建议使用单例Bean
-
如何避免循环依赖?
- 使用事件机制
- 使用观察者模式
- 使用接口隔离
- 合理划分模块
- 使用@Lazy注解
-
循环依赖对性能的影响
- 增加内存使用
- 延长启动时间
- 影响Bean的初始化顺序
- 可能导致并发问题
总结
Spring通过三级缓存机制解决了循环依赖问题,但建议在开发中尽量避免使用循环依赖。如果必须使用,应该:
- 使用单例Bean
- 避免构造器注入
- 使用setter注入或@Lazy注解
- 合理设计架构
- 注意性能影响
通过合理的设计和最佳实践,可以避免或最小化循环依赖带来的问题。