一、什么是 Spring 循环依赖?
1.1 定义与本质
循环依赖是指两个或多个 Spring Bean 之间存在相互引用的关系,形成一个依赖的闭环结构。这种依赖关系会导致 Bean 初始化过程中的"死锁"问题。具体表现为:
- 简单循环依赖:Bean A 依赖 Bean B,同时 Bean B 又依赖 Bean A,形成 A ↔ B 的依赖环
- 复杂循环依赖:Bean A 依赖 Bean B,Bean B 依赖 Bean C,而 Bean C 又依赖 Bean A,形成 A → B → C → A 的依赖链
- 自环依赖:Bean A 直接或间接地依赖自身(较少见)
本质分析:
- 初始化时序问题:Bean 的完整初始化需要其依赖项已经就绪,但依赖项同样需要等待当前 Bean 初始化完成
- 对象状态矛盾:Spring 需要完整构造对象实例,但循环依赖导致对象处于"半成品"状态
- 依赖解决机制:Spring 通过三级缓存(singletonFactories、earlySingletonObjects、singletonObjects)来打破这种循环
典型示例:
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB; // 依赖ServiceB
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA; // 又依赖ServiceA
}
1.2 循环依赖的三种场景
Spring 对不同类型的循环依赖有不同的处理能力:
1.2.1 构造器注入循环依赖
- 特征:通过构造函数参数相互注入
- 示例:
@Service public class ServiceA { private final ServiceB serviceB; public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; } } @Service public class ServiceB { private final ServiceA serviceA; public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; } } - Spring处理:直接抛出
BeanCurrentlyInCreationException,因为构造函数必须立即获得依赖对象
1.2.2 字段/setter注入循环依赖
- 特征:通过@Autowired字段或setter方法注入
- 示例:
@Service public class ServiceA { @Autowired private ServiceB serviceB; } @Service public class ServiceB { @Autowired private ServiceA serviceA; } - Spring处理流程:
- 开始创建
ServiceA(getSingleton 时) - 实例化
ServiceA(new 出来,半成品) - 将
ObjectFactory(用于创建ServiceA早期引用)放入三级缓存 - 开始填充属性,发现需要
ServiceB - 创建
ServiceB(getSingleton) - 实例化
ServiceB - 将
ServiceB的ObjectFactory放入三级缓存 - 填充
ServiceB属性时,发现需要ServiceA - 查找
ServiceA,发现正在创建中,从三级缓存获取ObjectFactory,调用getObject()获取ServiceA早期引用 - 将
ServiceA早期引用放入二级缓存,并从三级缓存移除 ServiceB完成属性注入(拿到ServiceA早期引用),继续初始化,最终放入一级缓存- 回到
ServiceA,注入ServiceB(已完全创建) ServiceA完成初始化,放入一级缓存
- 开始创建
1.2.3 单例与原型Bean循环依赖
- 特征:单例Bean依赖原型Bean,同时原型Bean又依赖该单例Bean
- Spring处理:无法自动解决,因为原型Bean不参与缓存
- 解决方案:
- 使用
@Lazy延迟加载 - 通过
ApplicationContext.getBean()手动获取 - 重构设计避免此类依赖
- 使用
1.3 循环依赖的危害与影响
1.3.1 直接危害
- 启动失败:抛出
BeanCurrentlyInCreationException,典型错误信息:Error creating bean with name 'serviceA': Requested bean is currently in creation: Is there an unresolvable circular reference? - 初始化阻塞:应用无法完成Spring容器初始化,导致服务不可用
1.3.2 潜在风险
-
内存泄漏:
- 在自定义BeanPostProcessor中错误处理可能导致对象重复创建
- 某些缓存机制可能保留无效的Bean引用
-
性能问题:
- 复杂的循环依赖会增加容器启动时间
- 运行时可能产生额外的代理对象
-
设计缺陷:
- 循环依赖往往是代码结构不合理的信号
- 可能导致业务逻辑混乱和测试困难
1.3.3 实际案例
某电商系统曾因循环依赖导致:
- 订单服务依赖库存服务
- 库存服务依赖促销服务
- 促销服务又依赖订单服务 结果:系统启动时需要额外5分钟解决依赖,且内存占用增加30%
最佳实践建议:
- 尽量避免循环依赖
- 如果必须使用,优先选择setter注入方式
- 对复杂依赖关系使用
@Lazy注解 - 定期使用
mvn dependency:tree分析依赖关系
二、Spring 如何解决循环依赖?核心原理:三级缓存
Spring 解决循环依赖的核心机制是三级缓存,通过提前暴露未初始化完成的 Bean 实例,打破依赖闭环。这种设计充分考虑了 Spring 框架中 Bean 生命周期的复杂性,以及 AOP 代理等高级特性的支持需求。下面我们先明确三级缓存的定义,再逐步分析其工作流程。
2.1 三级缓存的定义
在 Spring 的 DefaultSingletonBeanRegistry 类中,定义了三个核心的缓存集合,分别对应三级缓存:
// 一级缓存:存储完全初始化完成的单例Bean(可直接使用)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:存储早期暴露的Bean实例(已实例化,但未完成属性注入和初始化方法调用)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存:存储Bean的工厂对象(用于生成早期Bean实例,支持AOP代理)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// 正在创建中的Bean名称集合(标记Bean是否处于创建过程中,避免重复创建和循环依赖检测)
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
缓存级别详解:
-
一级缓存(singletonObjects):
- 存储的是完全初始化完成的单例Bean
- 这些Bean已经完成了所有生命周期步骤:
- 实例化(通过构造方法)
- 属性注入(@Autowired、@Resource等)
- 初始化方法调用(@PostConstruct)
- 实现了InitializingBean接口的afterPropertiesSet()方法
- 可以直接被应用程序使用
-
二级缓存(earlySingletonObjects):
- 存储的是早期暴露的Bean实例
- 这些Bean已经完成了实例化,但尚未完成:
- 属性注入
- 初始化方法调用
- 主要用于解决循环依赖问题
-
三级缓存(singletonFactories):
- 存储的是ObjectFactory对象
- 这些工厂对象可以生成早期Bean实例
- 关键区别:
- 二级缓存存储的是确定的Bean实例
- 三级缓存存储的是可以生成Bean实例的工厂
- 支持AOP代理的动态生成
特殊集合说明:
singletonsCurrentlyInCreation:这是一个线程安全的Set,用于记录当前正在创建的Bean名称。它的主要作用是:- 防止同一个Bean被重复创建
- 用于检测循环依赖
- 配合Spring的循环依赖异常抛出机制
2.2 三级缓存的工作流程(以 A 依赖 B,B 依赖 A 为例)
让我们通过一个具体的场景来理解三级缓存的工作机制:
场景设定:
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
// ...
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
// ...
}
详细工作流程:
步骤 1:初始化 Bean A
- Spring容器启动,开始创建Bean A
- 将"serviceA"加入
singletonsCurrentlyInCreation集合 - 调用ServiceA的构造方法,完成实例化
- 此时ServiceA实例已经存在,但serviceB字段为null
- 创建ObjectFactory对象并存入三级缓存
- 这个工厂的getObject()方法可以:
- 返回原始ServiceA实例
- 或者返回ServiceA的代理对象(如果需要AOP)
- 这个工厂的getObject()方法可以:
- 开始属性注入:
- 发现需要注入ServiceB
- 检查缓存:
- 一级缓存:无ServiceB
- 二级缓存:无ServiceB
- 三级缓存:无ServiceB
- 暂停ServiceA的创建,开始创建ServiceB
步骤 2:初始化 Bean B
- 将"serviceB"加入
singletonsCurrentlyInCreation集合 - 调用ServiceB的构造方法,完成实例化
- 创建ObjectFactory对象并存入三级缓存
- 开始属性注入:
- 发现需要注入ServiceA
- 检查缓存:
- 一级缓存:无ServiceA
- 二级缓存:无ServiceA
- 三级缓存:找到ServiceA的ObjectFactory
- 调用ObjectFactory.getObject():
- 如果ServiceA需要AOP代理,则生成代理对象
- 否则返回原始ServiceA实例
- 将获取到的ServiceA实例(可能是代理)放入二级缓存
- 完成ServiceB的属性注入
- 执行ServiceB的初始化方法:
- @PostConstruct方法
- afterPropertiesSet()方法
- 将完整的ServiceB实例存入一级缓存
- 从二级和三级缓存中移除ServiceB相关数据
- 从
singletonsCurrentlyInCreation移除"serviceB"
步骤 3:完成 Bean A 的初始化
- 继续ServiceA的属性注入:
- 现在一级缓存中已有ServiceB
- 注入完整的ServiceB实例
- 执行ServiceA的初始化方法
- 将完整的ServiceA实例存入一级缓存
- 从二级缓存中移除ServiceA的早期实例
- 从三级缓存中移除ServiceA的ObjectFactory
- 从
singletonsCurrentlyInCreation移除"serviceA"
关键点总结:
- 缓存升级机制:Bean实例会随着其完成度的提高,从三级缓存逐步升级到一级缓存
- 早期暴露:在实例化后立即通过三级缓存暴露Bean,即使它还不完整
- 代理支持:三级缓存中的ObjectFactory可以智能处理AOP代理需求
- 线程安全:整个过程需要考虑并发情况,使用ConcurrentHashMap等线程安全集合
2.3 为什么需要三级缓存?二级缓存不够吗?
深入分析缓存层级必要性:
-
二级缓存的局限性:
- 假设只有二级缓存:
- Bean A实例化后,直接将原始实例放入二级缓存
- 当Bean B依赖Bean A时,获取到的是原始实例
- 如果Bean A需要AOP代理,最终应该使用代理实例
- 这就导致系统中存在两个Bean A实例:
- 原始实例(被Bean B引用)
- 代理实例(最终版本)
- 破坏了单例原则,可能导致不可预期的行为
- 假设只有二级缓存:
-
三级缓存的优势:
- 通过ObjectFactory延迟决定返回实例的类型:
- 在真正需要时(getObject()调用时)才决定:
- 是否需要创建代理
- 是返回原始实例还是代理实例
- 在真正需要时(getObject()调用时)才决定:
- 确保整个系统中:
- 对于需要代理的Bean,所有依赖方都使用同一个代理实例
- 对于普通Bean,所有依赖方都使用同一个原始实例
- 通过ObjectFactory延迟决定返回实例的类型:
-
AOP代理的特殊处理:
- 代理创建时机:
- 传统情况:在Bean初始化完成后创建代理
- 循环依赖情况:需要在早期就决定是否创建代理
- AbstractAutowireCapableBeanFactory的getEarlyBeanReference方法:
protected Object getEarlyBeanReference(String beanName, Object bean) { Object exposedObject = bean; if (!Boolean.FALSE.equals(this.earlyProxyReferences.get(beanName))) { exposedObject = resolveBeforeInstantiation(beanName, bean); } return exposedObject; } - 这个方法会:
- 检查是否需要提前AOP代理
- 如果需要,则创建并返回代理对象
- 否则返回原始对象
- 代理创建时机:
-
性能考量:
- 三级缓存的设计实际上是一种懒加载策略:
- 不是所有Bean都需要提前创建代理
- 只有发生循环依赖时才会调用ObjectFactory
- 减少了不必要的代理创建开销
- 三级缓存的设计实际上是一种懒加载策略:
结论: 三级缓存是Spring框架中一个精妙的设计,它:
- 完美解决了循环依赖问题
- 保持了单例原则
- 支持AOP代理等高级特性
- 兼顾了性能考量
三、Spring 无法解决的循环依赖场景深度解析
Spring 框架通过三级缓存机制(singletonFactories、earlySingletonObjects、singletonObjects)能够有效解决大多数单例Bean的循环依赖问题。然而,在某些特定场景下,循环依赖问题仍然无法自动解决。下面我们将详细分析这些特殊场景,探讨其背后的原理,并提供可行的解决方案。
3.1 构造器注入循环依赖
问题现象
当两个Bean通过构造器方式互相注入时,Spring容器会直接抛出BeanCurrentlyInCreationException,表明检测到了无法解决的循环依赖。
技术原理
Spring解决循环依赖的核心机制是提前暴露未完成初始化的Bean实例。但对于构造器注入:
- 构造器注入发生在Bean实例化阶段
- 必须首先获得所有构造参数才能创建实例
- 无法先创建不完整的实例再补充依赖
典型场景示例
@Component
public class OrderService {
private final UserService userService;
@Autowired
public OrderService(UserService userService) {
this.userService = userService;
}
}
@Component
public class UserService {
private final OrderService orderService;
@Autowired
public UserService(OrderService orderService) {
this.orderService = orderService;
}
}
解决方案对比
| 方案 | 实现方式 | 适用场景 | 缺点 |
|---|---|---|---|
| 字段注入 | 使用@Autowired标注字段 | 简单场景 | 不利于单元测试 |
| Setter注入 | 使用@Autowired标注setter方法 | 需要动态变更依赖 | 代码稍显冗长 |
| @Lazy注解 | 在构造参数前加@Lazy | 必须保持构造器注入 | 代理对象可能影响性能 |
最佳实践建议
对于必须使用构造器注入的场景,推荐组合使用@Lazy和构造器注入:
@Component
public class OrderService {
private final UserService userService;
@Autowired
public OrderService(@Lazy UserService userService) {
this.userService = userService;
}
}
3.2 原型Bean循环依赖
问题本质
原型(Prototype)Bean的特殊性在于:
- 每次请求都创建新实例
- 不参与任何级别的缓存
- 完整的生命周期每次都会执行
问题复现流程
- 请求获取原型Bean A
- 开始创建A实例
- 发现需要注入原型Bean B
- 请求获取原型Bean B
- 开始创建B实例
- 发现需要注入原型Bean A
- 再次请求获取原型Bean A...
- 最终栈溢出或抛出异常
应用场景示例
@Component
@Scope("prototype")
public class PrototypeA {
@Autowired
private PrototypeB b;
}
@Component
@Scope("prototype")
public class PrototypeB {
@Autowired
private PrototypeA a;
}
解决方案实现
使用JSR-330的Provider接口:
@Component
@Scope("prototype")
public class PrototypeA {
@Autowired
private Provider<PrototypeB> bProvider;
public void execute() {
PrototypeB b = bProvider.get();
// 使用b执行业务逻辑
}
}
方案对比分析
| 方案 | 优点 | 缺点 | 适用版本 |
|---|---|---|---|
| Provider | 标准JSR-330方案 | 需要手动调用get() | Spring 4.3+ |
| ObjectFactory | Spring原生方案 | 非标准接口 | 全版本支持 |
| 方法注入 | 自动处理 | 仅适用于方法调用 | 全版本支持 |
3.3 混合作用域循环依赖
问题特征
当作用域不同的Bean形成循环依赖链时,如:
- 单例Bean A → 原型Bean B → 单例Bean A
- 请求域Bean X → 单例Bean Y → 请求域Bean X
底层机制
- 单例Bean A开始初始化
- 需要注入原型Bean B
- 开始创建原型Bean B
- 需要注入单例Bean A
- 但单例Bean A尚未完成初始化
- 无法通过缓存获取完整实例
典型业务场景
- 单例的缓存服务依赖原型的处理器
- 原型的处理器又依赖缓存的单例服务
- 形成跨作用域的循环引用
解决方案示例
使用ApplicationContextAware接口:
@Component
@Scope("prototype")
public class PrototypeProcessor implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext context) {
this.context = context;
}
public void process() {
CacheService cacheService = context.getBean(CacheService.class);
// 使用缓存服务
}
}
设计建议
- 尽量避免跨作用域的Bean依赖
- 考虑使用事件驱动模型替代直接依赖
- 将共享逻辑提取到无状态组件中
- 使用门面模式封装复杂依赖关系
四、实际开发中如何排查与解决循环依赖?
4.1 循环依赖的排查方法
当 Spring 容器启动时抛出 BeanCurrentlyInCreationException,可以通过以下步骤排查循环依赖:
步骤 1:查看异常堆栈信息
异常堆栈中会明确提示 "循环依赖的 Bean 链条",例如:
org.springframework.beans.factory.BeanCurrentlyInCreationException:
Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:296)
...
Caused by: org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'a': Injection of autowired dependencies failed;
...
通过堆栈信息,可以定位到正在创建的 Bean(如上述的 "a"),以及其依赖的 Bean。关键信息包括:
- 当前正在创建的 Bean 名称
- 依赖注入失败的具体位置
- 完整的调用链,展示 Bean 之间的依赖关系
步骤 2:分析 Bean 的依赖关系
可以通过以下方式梳理 Bean 之间的依赖关系:
-
代码静态分析:
- 查看代码中的注入注解:检查
@Autowired、@Resource、@Inject等注解,明确每个 Bean 依赖哪些其他 Bean。 - 使用 IDE 的 "Find Usages" 功能,查找某个 Bean 被哪些其他 Bean 引用。
- 查看代码中的注入注解:检查
-
运行时分析工具:
- 使用 Spring Boot Actuator:如果是 Spring Boot 项目,可以引入
spring-boot-starter-actuator,通过/actuator/beans端点查看所有 Bean 的依赖关系(需要在配置文件中开启端点:management.endpoints.web.exposure.include=beans)。 - 使用 Spring 自带的
DefaultListableBeanFactory:通过getDependenciesForBean()方法获取指定 Bean 的所有依赖。
- 使用 Spring Boot Actuator:如果是 Spring Boot 项目,可以引入
-
可视化工具:
- 使用 IDEA 插件:如 "Spring Assistant" 或 "Spring Beans",可以可视化展示 Bean 之间的依赖关系,快速定位循环依赖。
- 使用 UML 工具绘制类图:通过绘制类之间的关系图,直观地发现循环引用。
步骤 3:确定循环依赖的类型
根据排查结果,判断循环依赖属于哪种类型:
-
构造器注入循环依赖:
- 两个 Bean 都通过构造器相互依赖
- Spring 无法处理这种循环依赖,必须修改设计
-
属性注入循环依赖:
- 通过 setter 方法或字段注入的循环依赖
- Spring 可以自动处理单例 Bean 的这种循环依赖
-
原型(Prototype)作用域循环依赖:
- 至少有一个 Bean 是原型作用域
- 需要特殊处理
-
间接循环依赖:
- 通过多个 Bean 间接形成的循环依赖链
- 需要分析整个依赖链条
4.2 常见的解决思路
1. 调整注入方式
- 优先使用字段注入或 setter 注入:
- 避免使用构造器注入,尤其是在存在多依赖的场景下
- Spring 对字段/setter 注入的循环依赖有内置解决方案
2. 延迟初始化
- 使用
@Lazy注解延迟初始化:- 对于构造器注入的循环依赖,在依赖参数上添加
@Lazy - 让 Spring 生成代理对象,推迟真实 Bean 的创建
- 适用于不频繁使用的依赖
- 对于构造器注入的循环依赖,在依赖参数上添加
3. 使用工厂模式
- 使用
Provider或ObjectFactory延迟获取依赖:- 对于原型 Bean 或复杂依赖
- 通过
Provider.get()或ObjectFactory.getObject()延迟获取 Bean - 避免在初始化时直接注入
- 示例:
@Service public class OrderService { private final Provider<UserService> userServiceProvider; public OrderService(Provider<UserService> userServiceProvider) { this.userServiceProvider = userServiceProvider; } public void processOrder() { UserService userService = userServiceProvider.get(); // 使用userService } }
4. 重构设计
- 拆分 Bean 的职责:
- 如果循环依赖是由于 Bean 的职责过于复杂导致的
- 可以将 Bean 拆分为多个职责单一的 Bean
- 从根本上消除循环依赖
- 示例:
- 将
UserOrderService拆分为UserService和OrderService - 提取公共逻辑到第三个服务中
- 将
5. 控制创建顺序
- 使用
@DependsOn指定 Bean 的创建顺序:- 对于非循环依赖但创建顺序有要求的场景
- 可以通过
@DependsOn强制指定 Bean 的创建顺序 - 注意:该注解无法解决循环依赖,仅用于调整创建顺序
- 示例:
@Service @DependsOn("databaseInitializer") public class UserService { // ... }
6. 使用接口解耦
- 面向接口编程:
- 定义清晰的接口边界
- 通过接口而非具体实现类来声明依赖
- 降低耦合度
4.3 实战案例:解决构造器注入循环依赖
假设我们有以下循环依赖场景:
// 订单服务
@Service
public class OrderService {
private final UserService userService;
// 构造器注入用户服务
public OrderService(UserService userService) {
this.userService = userService;
}
}
// 用户服务
@Service
public class UserService {
private final OrderService orderService;
// 构造器注入订单服务
public UserService(OrderService orderService) {
this.orderService = orderService;
}
}
解决方案 1:使用@Lazy注解
@Service
public class OrderService {
private final UserService userService;
// 在依赖参数上添加@Lazy注解,延迟UserService的初始化
public OrderService(@Lazy UserService userService) {
this.userService = userService;
}
// 业务方法(调用userService时才会触发真实实例的创建)
public void createOrder(Long userId) {
// 此时userService的真实实例才会被初始化
String userName = userService.getUserName(userId);
System.out.println("为用户【" + userName + "】创建订单");
}
}
@Service
public class UserService {
private final OrderService orderService;
// 构造器注入OrderService(无需额外处理,因OrderService已通过@Lazy延迟依赖)
public UserService(OrderService orderService) {
this.orderService = orderService;
}
// 业务方法
public String getUserName(Long userId) {
// 模拟从数据库获取用户名
return "用户" + userId;
}
}
案例说明
原理:
@Lazy注解会让 Spring 为UserService生成一个动态代理对象- 在
OrderService构造器注入时,注入的是代理对象而非真实实例 - 只有当调用
userService.getUserName()时,代理对象才会触发真实UserService实例的创建 - 这打破了"创建 OrderService 需先有 UserService,创建 UserService 需先有 OrderService"的闭环
验证:
- 启动 Spring 容器后,调用
orderService.createOrder(1L) - 控制台会正常输出"为用户【用户1】创建订单"
- 无循环依赖异常
解决方案 2:使用 ApplicationContextAware
@Service
public class OrderService implements ApplicationContextAware {
private ApplicationContext context;
private UserService userService;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.context = applicationContext;
}
// 通过getter延迟获取UserService
public UserService getUserService() {
if (userService == null) {
userService = context.getBean(UserService.class);
}
return userService;
}
public void createOrder(Long userId) {
String userName = getUserService().getUserName(userId);
System.out.println("为用户【" + userName + "】创建订单");
}
}
@Service
public class UserService {
private final OrderService orderService;
public UserService(OrderService orderService) {
this.orderService = orderService;
}
public String getUserName(Long userId) {
return "用户" + userId;
}
}
优缺点:
- 优点:完全解除了构造器循环依赖
- 缺点:引入了对Spring容器的依赖,代码不够优雅
最佳实践建议
-
预防胜于治疗:
- 在设计阶段就避免循环依赖
- 遵循单一职责原则
- 使用清晰的层次架构
-
监控与告警:
- 在CI/CD流程中加入循环依赖检查
- 使用工具如
ArchUnit进行架构测试
-
文档记录:
- 对必要的循环依赖进行文档说明
- 解释采用当前解决方案的原因
-
定期重构:
- 定期审查代码中的循环依赖
- 随着业务发展,可能有机会消除之前不得不保留的循环依赖
五、Spring 循环依赖的核心源码解析
5.1 核心入口:AbstractBeanFactory.getBean()
getBean()方法是Spring容器获取Bean实例的核心入口,整个Bean生命周期管理都从这里开始。其核心逻辑通过doGetBean()方法实现,具体步骤如下:
-
Bean名称处理:
- 转换别名到规范名称(如处理"&"前缀的FactoryBean名称)
- 示例:输入别名"myAlias"会被转换为注册的真实beanName
-
三级缓存查询:
- 优先检查完全初始化的单例
- 如果未找到且该Bean正在创建中,会尝试获取早期引用
-
原型Bean校验:
if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCreationException("原型Bean循环依赖检测失败"); }- Spring不支持原型Bean的循环依赖,会立即抛出异常
-
父容器查询:
- 如果当前容器没有该Bean定义,会递归查询父容器
- 典型应用场景:Spring MVC中DispatcherServlet容器会委托父容器查找Service层Bean
-
依赖处理:
- 解析
@DependsOn注解声明的依赖关系 - 确保依赖Bean先创建,如果检测到循环依赖会抛出异常
- 解析
-
作用域处理:
- 单例(Singleton):通过
getSingleton()方法管理 - 原型(Prototype):每次调用都创建新实例
- 自定义作用域:如Request/Session作用域,通过Scope接口实现
- 单例(Singleton):通过
5.2 三级缓存查询逻辑:DefaultSingletonBeanRegistry.getSingleton()
Spring通过三级缓存机制解决循环依赖问题,具体实现如下:
缓存结构
-
一级缓存(singletonObjects):
- 存储完全初始化好的单例Bean
- ConcurrentHashMap实现,键为beanName,值为Bean实例
-
二级缓存(earlySingletonObjects):
- 存储早期暴露的Bean实例(已实例化但未完成属性注入)
- HashMap实现,需要同步控制
-
三级缓存(singletonFactories):
- 存储ObjectFactory工厂对象
- 可以生成早期代理对象(处理AOP场景)
查询流程示例
假设BeanA依赖BeanB,BeanB又依赖BeanA:
- 创建BeanA时,实例化后立即将ObjectFactory放入三级缓存
- 当BeanB需要注入BeanA时,会通过三级缓存获取早期引用
- 最终完成初始化后,BeanA会从三级缓存升级到一级缓存
线程安全实现
synchronized (this.singletonObjects) {
// 双重检查锁定模式
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 查询二级缓存逻辑...
}
}
5.3 早期实例暴露:AbstractAutowireCapableBeanFactory.doCreateBean()
doCreateBean()方法完成Bean实例的核心创建工作:
-
实例化阶段:
- 通过反射调用构造函数创建原始对象
- 示例:
instanceWrapper = createBeanInstance(beanName, mbd, args)
-
提前暴露引用:
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));- 将原始对象包装为ObjectFactory放入三级缓存
- 处理AOP代理时会在这里生成早期代理对象
-
属性填充:
- 处理
@Autowired等注解的依赖注入 - 可能触发依赖Bean的创建,形成循环依赖
- 处理
-
初始化阶段:
- 调用初始化方法(@PostConstruct等)
- 执行AOP代理的最终生成
-
缓存升级:
- 完成所有初始化后,将Bean从三级缓存移至一级缓存
- 清理早期引用相关缓存
典型时序图
创建BeanA -> 实例化 -> 暴露引用 -> 注入BeanB
-> 创建BeanB -> 实例化 -> 注入BeanA(从缓存获取早期引用)
-> 完成BeanB初始化
-> 完成BeanA初始化
六、Spring 循环依赖的常见误区与最佳实践
6.1 常见误区
误区1:"禁用循环依赖后,所有循环依赖都会报错"
Spring框架提供了allowCircularReferences属性(自Spring Boot 2.6版本起默认禁用),用于控制是否允许循环依赖。但需要特别注意:
-
禁用后的实际影响范围:
- 仅会阻断"单例Bean字段注入循环依赖"的自动解决机制
- 对于构造器注入的循环依赖、原型(prototype)作用域Bean的循环依赖,即使启用该属性也会报错
-
原因分析:
- 构造器注入:在Bean实例化阶段就需要完整依赖,无法通过三级缓存机制解决
- 原型Bean:每次获取都会创建新实例,Spring无法保证其依赖关系的正确性
误区2:"三级缓存可以解决所有单例Bean循环依赖"
三级缓存机制虽然强大,但存在局限性:
-
典型问题场景:
@Component public class A { @Autowired private B b; @PostConstruct public void init() { b.doSomething(); // 危险操作! } } @Component public class B { @Autowired private A a; @Autowired private UserDao userDao; // 可能尚未注入 public void doSomething() { userDao.query(); // 可能抛出NPE } } -
根本原因:
- A的
@PostConstruct方法在属性注入后立即执行 - 此时B可能仍处于属性注入或初始化阶段
- B的
doSomething()方法依赖的userDao可能尚未完成注入
- A的
-
解决方案:
- 避免在初始化方法中调用依赖Bean的业务方法
- 使用
@Lazy延迟依赖Bean的初始化 - 通过
ApplicationListener在容器完全启动后执行逻辑
误区3:"使用@DependsOn可以解决循环依赖"
关于@DependsOn的正确认识:
-
实际作用:
- 仅强制指定Bean的创建顺序(如
@DependsOn("b")表示A必须在B之后创建) - 无法解决循环依赖问题
- 仅强制指定Bean的创建顺序(如
-
限制场景:
@Component @DependsOn("b") public class A { @Autowired private B b; } @Component @DependsOn("a") public class B { @Autowired private A a; }- 这种互相
@DependsOn的配置仍会抛出异常
- 这种互相
-
适用场景:
- 非循环依赖但有创建顺序要求的情况
- 例如:A需要使用B初始化后的某些资源
误区4:"循环依赖一定是代码设计问题,必须完全避免"
循环依赖的辩证看待:
-
不合理的情况:
- 服务层之间无节制的互相调用
- 明显的职责划分不清
-
可能合理的情况:
- 订单服务与支付服务的双向依赖:
- 订单创建需要调用支付接口
- 支付结果回调需要更新订单状态
- 领域模型中某些固有关系的表达
- 订单服务与支付服务的双向依赖:
-
处理原则:
- 优先考虑通过设计消除不必要的循环
- 对于必要的循环依赖,确保依赖调用的时机在Bean完全初始化后
6.2 最佳实践
1. 优先选择字段注入或Setter注入,谨慎使用构造器注入
注入方式对比:
| 注入方式 | 循环依赖支持 | 代码简洁度 | 适用场景 |
|---|---|---|---|
| 字段注入 | 支持 | 高 | 大多数场景 |
| Setter注入 | 支持 | 中 | 可选依赖 |
| 构造器注入 | 不支持 | 低 | 无循环的必选依赖 |
构造器注入的特殊处理:
@Component
public class A {
private final B b;
@Autowired
public A(@Lazy B b) { // 必须配合@Lazy使用
this.b = b;
}
}
2. 合理使用@Lazy注解
使用要点:
- 作用目标:可以标注在字段、方法参数或配置类上
- 工作原理:创建代理对象,延迟真实Bean的初始化
- 注意事项:确保首次方法调用时Bean已完全初始化
典型应用:
@Service
public class OrderService {
@Lazy
@Autowired
private PaymentService paymentService; // 延迟初始化
}
3. 原型Bean依赖的特殊处理
解决方案对比:
| 方案 | 所属包 | 特点 |
|---|---|---|
| Provider | javax.inject | 标准JSR-330 |
| ObjectFactory | org.springframework | Spring原生 |
实现示例:
@Component
@Scope("prototype")
public class ReportGenerator {
@Autowired
private SingletonService singletonService;
}
@Component
public class SingletonService {
@Autowired
private Provider<ReportGenerator> reportGeneratorProvider;
public void generateReport() {
ReportGenerator generator = reportGeneratorProvider.get();
generator.generate();
}
}
4. 拆分复杂Bean
重构策略:
-
职责分析:
- 识别Bean中可独立的功能模块
- 评估依赖关系的合理性
-
事件驱动改造:
// 事件定义 public class OrderPaidEvent { private Long orderId; private BigDecimal amount; // getters/setters } // 支付服务 @Service public class PaymentService { @Autowired private ApplicationEventPublisher publisher; public void processPayment(Order order) { // 处理支付逻辑... publisher.publishEvent(new OrderPaidEvent(order.getId(), order.getAmount())); } } // 订单服务 @Service public class OrderService { @EventListener public void handlePayment(OrderPaidEvent event) { // 更新订单状态 } }
5. 安全的初始化时机控制
实现方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
@PostConstruct + CompletableFuture | 灵活控制 | 代码稍复杂 |
ApplicationListener | 官方支持 | 全局处理 |
示例代码:
@Component
public class SystemInitializer implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private CacheService cacheService;
@Autowired
private ConfigService configService;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 确保所有Bean已初始化完成
cacheService.warmUp();
configService.loadRemoteConfig();
}
}
6. 循环依赖检测手段
全周期检测方案:
-
开发阶段:
- IDEA安装Spring Assistant插件
- 使用Dependency Analyzer分析依赖图
-
测试阶段:
@SpringBootTest class ContextLoadsTest { @Test void contextLoads() { // 如果存在未处理的循环依赖,测试将失败 } } -
生产环境:
- 启用Spring Boot Actuator
- 定期检查
/actuator/beans端点 - 集成监控告警系统
依赖分析示例输出:
+------------------+ +------------------+
| OrderService | <-------> | PaymentService |
+------------------+ +------------------+
^ ^
| |
v v
+------------------+ +------------------+
| UserService | | AuditService |
+------------------+ +------------------+
193

被折叠的 条评论
为什么被折叠?



