救命!Spring 启动又崩了?!循环依赖又踩坑

循环依赖问题:当循环依赖遇上 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() {
        // 扣库存
        // 建订单
        // 扣款
    }
}

其调用过程如下:

sequenceDiagram participant Client participant Proxy as 代理对象 participant Target as 原始OrderService Client->>Proxy: placeOrder() Proxy->>Proxy: AOP检查 → 开启数据库事务 Proxy->>Target: 调用原始placeOrder()方法 Target-->>Proxy: 方法执行完毕 Proxy->>Proxy: 提交/回滚事务 Proxy-->>Client: 返回结果
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)
sequenceDiagram participant Spring as Spring容器 participant A as Service A (QuestionAnswerService) participant B as Service B (AsyncRegradeTaskService) Spring->>A: 1. 创建 A(new) A-->>Spring: 2. 将 A 的 ObjectFactory 放入三级缓存 Spring->>A: 3. 填充 A 属性 → 需要 B Spring->>B: 4. 创建 B(new) B-->>Spring: 5. 将 B 的 ObjectFactory 放入三级缓存 Spring->>B: 6. 填充 B 属性 → 需要 A Spring->>Spring: 7. 从三级缓存获取 A 的工厂 Spring->>A: 8. 调用 factory.getObject() → 获取原始 A Spring-->>B: 9. 将原始 A 注入 B B-->>Spring: 10. B 初始化完成 → 放入一级缓存 Spring-->>A: 11. 将完整 B 注入 A A-->>Spring: 12. A 初始化完成 → 放入一级缓存 Note right of Spring: ✅ 循环依赖成功解决

成功的关键:注入的是原始对象(raw instance),不需要代理。


含 AOP 的循环依赖为何失败?

当 B 带有 @Async(或 @Transactional)时,Spring 必须为其创建 代理对象 。但代理只能在 B 完全初始化后生成。

而在循环依赖中:

  • A 创建时需要 B;
  • B 创建时需要 A;
  • Spring 试图从三级缓存中提供 B 的"早期引用" → 但此时只有 原始 B ,没有 代理 B
  • A 注入的是原始 B,调用 @Async 方法时 不会触发异步(因为没走代理);
  • 更严重的是:Spring 在创建 B 时发现它需要代理,但又无法在"半成品"阶段生成代理 → 抛出异常。
含 AOP 的循环依赖失败流程
sequenceDiagram participant Spring as Spring容器 participant A as Service A participant B as Service B (@Async) Spring->>A: 1. 创建 A(new) A-->>Spring: 2. A 的工厂放入三级缓存 Spring->>A: 3. 填充 A 属性 → 需要 B Spring->>B: 4. 创建 B(new) B-->>Spring: 5. B 的工厂放入三级缓存 Spring->>B: 6. 填充 B 属性 → 需要 A Spring->>Spring: 7. 从三级缓存获取 A 的原始对象 Spring-->>B: 8. 将原始 A 注入 B Spring->>B: 9. B 初始化 → 需为 @Async 创建代理 Note over Spring,B: ⚠️ 代理创建需 B 完整,但 B 尚未完成! Spring->>A: 10. A 继续初始化 → 需要完整 B Spring->>B: 11. 请求 B 实例 Note over Spring: B 仍在创建中 → 循环依赖未解 Spring-->>A: 12. 抛出 BeanCurrentlyInCreationException Note right of Spring: ❌ 启动失败:无法生成 B 的代理

即使 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 解决的问题,往往说明你的模块耦合太紧。"

------ 好的设计,本就不该有循环依赖。

相关推荐
程序员爱钓鱼2 小时前
Python编程实战:综合项目 —— Flask 迷你博客
后端·python·面试
程序员爱钓鱼2 小时前
Python编程实战:综合项目 —— 迷你爬虫项目
后端·python·面试
Qiuner2 小时前
Spring Boot 进阶:application.properties 与 application.yml 的全方位对比与最佳实践
java·spring boot·后端
leonardee2 小时前
【玩转全栈】----Django基本配置和介绍
java·后端
绝无仅有3 小时前
电商大厂面试题解答与场景解析(二)
后端·面试·架构
绝无仅有3 小时前
某电商大厂场景面试相关的技术文章
后端·面试·架构
李昊哲小课3 小时前
手写 Spring Boot 嵌入式Tomcat项目开发教学
spring boot·后端·tomcat
IT_陈寒3 小时前
React性能优化实战:我用这5个技巧将组件渲染速度提升了70%
前端·人工智能·后端
程序员三明治3 小时前
SpringBoot YAML 配置读取机制 + 数据库自动初始化原理
数据库·spring boot·后端