2026-04-09 购物助手AI搜索技术之Spring循环依赖全解析

小编头像

小编

管理员

发布于:2026年04月14日

26 阅读 · 0 评论

在构建大型Java后端系统的过程中,Spring框架的循环依赖问题是每个开发者绕不开的技术难关。而值得关注的是,购物助手AI背后复杂的微服务调用链路同样充斥着模块间的相互引用。这就好比购物助手AI中的检索增强生成(RAG)需要多个服务模块互相协作——若A服务依赖B服务,B服务又依赖A服务,容器在创建这些Bean时就会陷入“先有鸡还是先有蛋”的困局。本文将从底层原理出发,深入拆解Spring三级缓存机制,配套完整代码示例与面试考点,帮助读者彻底搞懂循环依赖问题。

一、痛点切入:为什么需要解决循环依赖

在日常开发中,循环依赖是一个高频出现的场景-2。比如,在购物助手AI的智能推荐模块中,用户意图识别服务可能依赖商品召回服务,而商品召回服务又依赖用户画像服务来获取偏好——这种依赖关系很容易形成闭环。

旧实现方式存在的问题

java
复制
下载
// 代码示例:典型的循环依赖场景
@Component
class AService {
    // A依赖B
    @Autowired
    private BService b;
}

@Component
class BService {
    // B依赖A,形成循环
    @Autowired
    private AService a;
}

如果不做特殊处理,项目启动时Spring会抛出BeanCurrentlyInCreationException异常-2。问题根源在于:容器需要先实例化A才能注入B,但B又需要A才能完成注入,形成“死锁”。

传统方式的缺陷

  1. 耦合过高:模块间直接相互引用,难以解耦

  2. 扩展性差:添加新功能时可能引入新的循环依赖

  3. 维护困难:代码依赖关系错综复杂,难以梳理

  4. 启动失败:项目无法正常启动,影响开发效率

Spring正是为了解决这一问题,设计了精妙的三级缓存机制,通过提前暴露“半成品Bean”的方式打破依赖闭环-2

二、核心概念讲解:什么是循环依赖

循环依赖(Circular Dependency) ,指的是两个或多个Bean之间相互持有对方的引用,形成闭环依赖关系-2

场景化类比

想象一下,两个工人互相等着对方先给自己递工具:工人A需要工人B的锤子才能干活,工人B需要工人A的扳手才能干活。两人都空着手站在工位上,谁也无法开始工作——这就是循环依赖的生动写照。

Spring的三级缓存机制,相当于在工人A刚开始搭架子时,就让他先把一个“半成品架子”放在旁边供工人B使用,这样B就能拿着这个架子继续干活,最终两人都能完成各自的任务。

Spring支持的循环依赖场景

依赖类型注入方式是否支持
构造器注入通过构造函数❌ 不支持
Setter注入通过setter方法✅ 支持
字段注入@Autowired注解✅ 支持

三、关联概念讲解:三级缓存(概念B)

三级缓存(Three-Level Cache) ,是Spring在DefaultSingletonBeanRegistry类中维护的三个Map结构,用于管理单例Bean在不同创建阶段的状态-2

三级缓存定义

缓存级别缓存名称存储内容作用
一级缓存singletonObjects完全初始化完成的成品Bean供业务直接使用
二级缓存earlySingletonObjects提前暴露的半成品Bean(已实例化但未完成属性填充)存放早期引用
三级缓存singletonFactoriesObjectFactory对象工厂按需生成Bean实例(代理或原始对象)

与循环依赖的关系

三级缓存是实现循环依赖解决方案的关键载体:

  • 概念A(循环依赖) 是问题本身

  • 概念B(三级缓存) 是Spring解决这一问题的具体技术手段

简单来说:循环依赖是“病”,三级缓存是“药” 。前者描述的是问题场景,后者是Spring框架给出的解决方案。

四、概念关系与区别总结

关系梳理

维度循环依赖三级缓存
角色定位问题(Problem)解决方案(Solution)
抽象层次设计层实现层
适用范围所有IoC容器都可能遇到Spring框架特有的实现机制
核心思想描述依赖闭环提前暴露+懒加载代理

一句话概括:循环依赖是Spring容器管理Bean时可能遇到的问题形态,而三级缓存是Spring为解决单例模式下Setter/字段注入循环依赖所设计的具体缓存架构。

五、代码示例:Spring如何解决循环依赖

完整可运行示例

java
复制
下载
// 1. 定义相互依赖的两个Bean
@Component
public class A {
    private B b;
    
    @Autowired
    public void setB(B b) {  // Setter注入(Spring支持此种方式解决循环依赖)
        this.b = b;
        System.out.println("A: B注入完成");
    }
    
    public void doSomething() {
        System.out.println("A执行方法");
    }
}

@Component
public class B {
    private A a;
    
    @Autowired
    public void setA(A a) {
        this.a = a;
        System.out.println("B: A注入完成");
    }
    
    public void doSomething() {
        System.out.println("B执行方法");
    }
}

// 2. 启动容器并测试
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);
        A a = context.getBean(A.class);
        B b = context.getBean(B.class);
        a.doSomething();
        b.doSomething();
    }
}

核心执行流程

创建Bean A时,Spring在doCreateBean()中会依次执行以下步骤-1

  1. 实例化:通过反射调用构造函数,创建A的“空对象”

  2. 提前暴露:将包含A的ObjectFactory放入三级缓存

  3. 属性填充:发现A依赖B,触发B的创建

  4. 递归创建B:B实例化后同样放入三级缓存,属性填充时发现依赖A

  5. 从缓存获取A的早期引用:从三级缓存获取ObjectFactory,调用getObject()得到A的引用,放入二级缓存

  6. 完成B的初始化:B注入完成后放入一级缓存

  7. 回到A:将已完成的B注入到A,完成A的初始化

关键源码逻辑

java
复制
下载
// DefaultSingletonBeanRegistry核心逻辑(基于Spring 5.3.x)
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 先从一级缓存获取成品
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 2. 再从二级缓存获取半成品
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            // 3. 最后从三级缓存获取ObjectFactory并生成对象
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                singletonObject = singletonFactory.getObject();
                this.earlySingletonObjects.put(beanName, singletonObject);
                this.singletonFactories.remove(beanName);
            }
        }
    }
    return singletonObject;
}

六、底层原理:技术支撑点

三级缓存机制的精妙之处在于,它充分利用了Java的以下底层技术:

1. 反射机制

Bean的实例化依赖反射调用构造函数,这是Spring IoC容器的基础能力-1

2. 函数式接口(ObjectFactory)

三级缓存不直接存储Bean实例,而是存储ObjectFactory——一个函数式接口:

java
复制
下载
@FunctionalInterface
public interface ObjectFactory<T> {
    T getObject();
}

存入的是一个Lambda表达式:() -> getEarlyBeanReference(beanName, mbd, bean),将“什么时候生成对象” 的决定权交给调用方。

3. AOP代理机制

三级缓存存在的最关键原因——当Bean需要被AOP代理时(如加了@Transactional),三级缓存中的ObjectFactory会在调用getObject()时动态决定返回原始对象还是代理对象-9

为什么二级缓存不够?

如果只用二级缓存,Spring必须在实例化后立刻决定是否生成代理,但此时还未执行BeanPostProcessor后置处理,无法判断是否需要AOP增强。三级缓存将代理决策延迟到第一次被其他Bean引用时,实现按需代理-9

七、高频面试题与参考答案

Q1:Spring是如何解决循环依赖的?

参考答案(简洁版):

Spring通过三级缓存机制和提前暴露半成品Bean引用的方式来解决单例作用域下Setter/字段注入的循环依赖问题。核心是在Bean实例化后、属性填充前,将包含Bean引用的ObjectFactory放入三级缓存,当其他Bean需要依赖时,从缓存中获取早期引用进行注入,从而打破依赖闭环。

踩分点: ①三级缓存具体哪三级 ②提前暴露的时机 ③ObjectFactory的作用 ④适用场景限制

Q2:为什么必须用三级缓存?二级不够吗?

参考答案:

二级缓存不足以解决AOP代理场景下的循环依赖问题。因为如果只用二级缓存,Spring必须在Bean实例化后立刻决定是否生成代理对象,但此时还未执行BeanPostProcessor后置处理,无法获知该Bean是否需要AOP增强。三级缓存存储的是ObjectFactory工厂,它将代理对象的创建延迟到第一次被其他Bean实际引用时,既能解决循环依赖,又能保证AOP代理正确生效。

踩分点: ①AOP代理的时机 ②延迟决策的设计思想 ③三级缓存vs二级缓存的本质区别

Q3:哪些类型的循环依赖Spring解决不了?

参考答案:

Spring的三级缓存机制无法解决以下两类循环依赖:

  1. 构造器注入的循环依赖:因为构造器注入要求在实例化时就完成所有依赖注入,无法实现“提前暴露”

  2. 多例(Prototype)作用域的循环依赖:多例Bean每次获取都会创建新实例,无法通过缓存复用,会导致无限递归

踩分点: ①能解决的前提条件 ②两种解决不了的场景 ③各场景的原因分析

Q4:三级缓存中各级缓存分别存什么?

参考答案:

  • 一级缓存singletonObjects):存放完全初始化完成的成品Bean,是最终的可用对象

  • 二级缓存earlySingletonObjects):存放提前暴露的半成品Bean(已实例化但未完成属性填充和初始化)

  • 三级缓存singletonFactories):存放ObjectFactory对象工厂,不直接存对象,仅在调用getObject()时才生成Bean实例

踩分点: ①三个Map的准确名称 ②各级存储内容的区别 ③读取顺序:一级→二级→三级

Q5:在实际开发中如何避免循环依赖?

参考答案:

  1. 优先使用构造器注入:构造器注入在编码阶段就能发现循环依赖,强制开发者遵循单一职责

  2. 使用@Lazy延迟加载:在其中一个依赖上添加@Lazy注解,让Spring延迟初始化该依赖

  3. 重新设计模块边界:将相互依赖的公共逻辑抽取到独立的中间服务中

  4. 使用Setter/字段注入但谨慎设计:仅在确认不会引入构造器循环依赖时使用

八、结尾总结

核心知识点回顾

  1. 循环依赖定义:Bean之间相互引用形成闭环,导致容器无法正常创建

  2. 三级缓存机制:三级缓存(成品池→半成品池→工厂池)是Spring的解决方案核心

  3. 解决前提:单例作用域 + 非构造器注入(Setter/字段注入)

  4. 三级原因:兼顾循环依赖破局与AOP代理的按需生成

  5. 无法解决场景:构造器注入 + 多例Bean

重点提示

  • 不要为了使用循环依赖而设计代码,它应该是不得已的情况,而非常规设计

  • 面试中除了回答三级缓存的机制,更要能说清楚“为什么必须是三级”以及“二级不够的原因”

  • 理解三级缓存的关键在于理解ObjectFactory延迟执行思想

下一篇预告

在后续文章中,我们将深入Spring IoC容器的Bean生命周期全流程,从源码级别剖析实例化、属性填充、初始化、销毁的完整链路,并结合购物助手AI场景下的实际生产案例,帮助你真正吃透Spring核心源码。

标签:

相关阅读