循环依赖问题:当循环依赖遇上 AOP
最近工作中遇到一个业务场景:教师修改试卷的正确答案后,系统需要异步重新批改所有学生已提交的答案。
- 主服务(实现类 A)负责更新标准答案并触发异步任务;
- 异步服务(服务 B)执行批量重判,并调用 A 中的辅助方法(如判断答案正误、计算得分等)。
结果应用启动时报错:
vbnet
BeanCurrentlyInCreationException:
Requested bean is currently in creation:
Is there an unresolvable circular reference?
这是一个典型的 循环依赖 + AOP(@Async / @Transactional) 问题。正常情况下,Spring 能通过三级缓存解决普通循环依赖,但一旦涉及 AOP,就无能为力了。下面我将从原理到实践完整梳理这个问题的来龙去脉和可行的解决方案。
根本原因:AOP 代理与三级缓存的冲突
要理解为什么失败,必须先搞清楚 Spring 的代理机制 和 Bean 创建流程。
Spring AOP 代理机制详解
什么是代理对象?
- 目标对象(Target) :你写的实现类实例(如
OrderService)。 - 代理对象(Proxy):Spring 动态生成的一个"包装类",外观与目标对象一致(实现相同接口或继承相同父类),但方法调用会被拦截,在前后插入额外逻辑(如事务、异步、日志等)。
java
@Service
public class OrderService {
@Transactional
public void placeOrder() {
// 扣库存
// 建订单
// 扣款
}
}
其调用过程如下:
Spring 的两种代理方式
| 代理类型 | 触发条件 | 实现方式 |
|---|---|---|
| JDK 动态代理 | 目标类实现了至少一个接口 | 生成 $Proxy0 类,实现相同接口,通过 InvocationHandler.invoke() 拦截 |
| CGLIB 代理 | 目标类没有接口 | 生成目标类的子类,重写非 final 方法,在方法中插入拦截逻辑 |
JDK 代理是"兄弟关系"(同接口),CGLIB 是"父子关系"(继承)。
代理对象何时创建?
Spring 在启动时会扫描所有 Bean,检查是否包含 AOP 注解(如 @Async、@Transactional)。这个过程由 BeanPostProcessor(后置处理器) 完成,例如:
AsyncAnnotationBeanPostProcessor(处理@Async)InfrastructureAdvisorAutoProxyCreator(处理@Transactional等)
它们会在 Bean 初始化阶段(post-process) 判断:
- 类或方法上是否有 AOP 注解?
- 是否启用了相关功能(如
@EnableAsync)? - 应该使用 JDK 还是 CGLIB 代理?
关键点 :
代理对象的创建不能在构造函数阶段完成,因为:
- 注解元数据需通过反射读取;
- 代理类型需根据类结构动态决定;
- AOP 配置(如切面表达式)也需在初始化后才能评估。
因此,代理对象是在 Bean 实例化 + 属性填充 + 初始化完成后才生成的。
三级缓存机制(无 AOP 场景)
对于普通单例 Bean,Spring 使用三级缓存解决循环依赖:
- 一级缓存(
singletonObjects):存放完全初始化好的 Bean。 - 二级缓存(
earlySingletonObjects):存放"早期引用"(已实例化,未初始化)。 - 三级缓存(
singletonFactories) :存放ObjectFactory,用于按需生成早期引用。
核心思想:允许在 Bean 尚未完全初始化前,将其"原始对象"提前暴露给其他依赖方。
普通循环依赖解决流程(A → B,B → A)
成功的关键:注入的是原始对象(raw instance),不需要代理。
含 AOP 的循环依赖为何失败?
当 B 带有 @Async(或 @Transactional)时,Spring 必须为其创建 代理对象 。但代理只能在 B 完全初始化后生成。
而在循环依赖中:
- A 创建时需要 B;
- B 创建时需要 A;
- Spring 试图从三级缓存中提供 B 的"早期引用" → 但此时只有 原始 B ,没有 代理 B;
- A 注入的是原始 B,调用
@Async方法时 不会触发异步(因为没走代理); - 更严重的是:Spring 在创建 B 时发现它需要代理,但又无法在"半成品"阶段生成代理 → 抛出异常。
含 AOP 的循环依赖失败流程
即使 A 不带 AOP,只要 B 带
@Async,且互相依赖,就会失败。
代码示例
主服务(Service A)
java
@Service
public class QuestionAnswerService { // ← A
@Autowired
private AsyncRegradeTaskService asyncTaskService; // 依赖 B
public void modifyStandardAnswer(AnswerUpdateRequest request) {
updateStandardAnswerInDB(request);
asyncTaskService.regradeAllSubmissionsAsync(request.getQuestionId(), request.getGroupId());
}
@Transactional
public void updateStandardAnswerInDB(AnswerUpdateRequest request) {
// DB 更新
}
// 被异步服务调用的辅助方法
public boolean evaluateSubmission(String studentAnswer, String correctAnswer) {
return studentAnswer.equals(correctAnswer);
}
}
异步服务(Service B)
java
@Service
public class AsyncRegradeTaskService { // ← B
@Autowired
private QuestionAnswerService mainService; // 依赖 A
@Async
@Transactional
public CompletableFuture<Void> regradeAllSubmissionsAsync(Long questionId, Long groupId) {
List<UserSubmission> submissions = fetchSubmissions(questionId, groupId);
for (UserSubmission sub : submissions) {
boolean correct = mainService.evaluateSubmission(sub.getContent(), getCorrectAnswer(questionId));
// ...
}
return CompletableFuture.completedFuture(null);
}
}
解决方案一:使用 @Lazy(临时修复)
java
@Service
public class QuestionAnswerService {
@Autowired
@Lazy // 👈 延迟加载
private AsyncRegradeTaskService asyncTaskService;
}
深入理解:为什么
@Lazy能解决 AOP 循环依赖?表面上看,
@Lazy和三级缓存都涉及"非直接注入真实对象",但它们的机制和目的截然不同:
- 三级缓存 在 Bean 创建过程中暴露一个原始对象(raw instance),用于解决普通循环依赖;
@Lazy则注入一个懒加载代理(lazy proxy),该代理在首次方法调用时才从容器获取真实 Bean。问题的关键在于:Spring 的 AOP 代理(如
@Async)必须在目标 Bean 完全初始化后才能创建。而在循环依赖场景中,若两个 Bean 互相依赖且其中一方需代理,Spring 就无法在"半成品"阶段提供有效的代理对象------三级缓存只能提供原始对象,而原始对象不具备 AOP 行为。
@Lazy的巧妙之处在于:它将依赖的解析时机从"Bean 创建期"推迟到"方法首次调用期"。此时,被依赖的 Bean 已经完成初始化并存在于一级缓存中,其代理也已生成,从而安全地满足 AOP 要求。换句话说,
@Lazy并没有改变 AOP 对"完整对象"的需求,而是绕开了创建阶段的依赖死锁。
解决方案二:重构设计(推荐长期方案)
抽取公共逻辑到独立服务:
java
@Service
public class GradingLogicService {
public boolean evaluateSubmission(String studentAnswer, String correctAnswer) {
return studentAnswer.equals(correctAnswer);
}
}
- A 和 B 都依赖
GradingLogicService; - 彻底消除 A ↔ B 的双向依赖。
✅ 代码更清晰、可测试、可维护。
补充:关于 Spring 代理的两个常见误区
1. 只有容器管理的对象才是代理
java
// 正确
OrderService service = context.getBean(OrderService.class); // 代理
// 错误
OrderService service = new OrderService(); // 无代理,@Transactional 失效
2. 同类方法调用绕过代理
java
@Service
public class MyService {
@Transactional
public void methodA() { ... }
public void methodB() {
methodA(); // this.methodA(),不走代理,事务失效!
}
}
总结
| 场景 | Spring 能否解决循环依赖 | 原因 |
|---|---|---|
| 普通单例 Bean(无 AOP) | ✅ 能 | 三级缓存提供原始对象作为早期引用 |
含 @Async / @Transactional 的 Bean |
❌ 不能 | 代理需完整对象,但循环依赖要求提前暴露"半成品" |
最佳实践建议:
- 优先通过 职责拆分 + 服务抽取 消除循环依赖;
- 临时方案可用
@Lazy,但不宜长期依赖; - 避免在 Service 内部直接调用自身带 AOP 的方法;
- 所有 Service 必须由 Spring 容器管理,禁止
new。
经验总结
"能用
@Lazy解决的问题,往往说明你的模块耦合太紧。"------ 好的设计,本就不该有循环依赖。