【大白话说Java面试题 第155题】【06_Spring篇】第15题:Spring 如何解决循环依赖问题?

📅 2026/7/6 1:49:55
【大白话说Java面试题 第155题】【06_Spring篇】第15题:Spring 如何解决循环依赖问题?
PDF大白话说Java面试题 — 06_Spring篇第15题Spring 如何解决循环依赖问题回答核心考点 Spring 循环依赖是面试中最经典、最深入的问题之一大厂面试不会只问三级缓存是什么而是深入考察三级缓存的源码级结构singletonObjects/earlySingletonObjects/singletonFactories的字段定义与类型、循环依赖的完整解决流程从getBean()到createBean()到populateBean()的源码链路、为什么需要三级缓存而非二级AOP 代理对象的提前暴露问题、构造器循环依赖为什么无法解决ConstructorResolver的创建时机、以及Lazy延迟注入和ObjectFactory的替代方案。面试官真正想判断的是你是否能从源码层面理解 Spring 解决循环依赖的设计精妙之处以及能否在生产中识别和解决循环依赖引发的启动失败、代理失效等问题。1. 什么是循环依赖循环依赖是指两个或多个 Bean 之间相互持有对方的引用形成闭环A → B → A两两循环 A → B → C → A三角循环Spring 中的循环依赖类型循环依赖类型示例Spring 是否支持原因构造器循环依赖A(B b)→B(A a)❌不支持构造器注入时 Bean 尚未实例化无法提前暴露Setter/字段循环依赖Autowired B b→Autowired A a✅支持实例化后可提前暴露半成品再注入依赖多例prototype循环依赖Scope(prototype)❌不支持原型 Bean 不缓存每次创建都是新实例DependsOn 循环依赖DependsOn(b)→DependsOn(a)❌不支持DependsOn强制顺序循环则报错2. 三级缓存的源码级结构Spring 的三级缓存在DefaultSingletonBeanRegistry中定义publicclassDefaultSingletonBeanRegistryextendsSimpleAliasRegistryimplementsSingletonBeanRegistry{// 一级缓存成品单例池完全初始化好的 BeanprivatefinalMapString,ObjectsingletonObjectsnewConcurrentHashMap(256);// 二级缓存提前暴露的对象半成品用于解决循环依赖privatefinalMapString,ObjectearlySingletonObjectsnewConcurrentHashMap(16);// 三级缓存单例工厂用于创建 Bean 或代理对象privatefinalMapString,ObjectFactory?singletonFactoriesnewHashMap(16);// 正在创建中的 Bean 名称集合用于检测循环依赖privatefinalSetStringsingletonsCurrentlyInCreationCollections.newSetFromMap(newConcurrentHashMap(16));}缓存字段名类型存放内容作用一级缓存singletonObjectsConcurrentHashMapString, Object完全初始化好的 Bean成品对外提供 Bean 实例二级缓存earlySingletonObjectsConcurrentHashMapString, Object已实例化但未初始化的 Bean半成品解决循环依赖避免重复创建代理三级缓存singletonFactoriesHashMapString, ObjectFactory?ObjectFactory可生成 Bean 或代理延迟创建代理对象支持 AOP3. 三级缓存解决循环依赖的完整流程以经典的A → B → A字段注入循环依赖为例完整梳理源码链路Step 1获取 A开始创建// AbstractBeanFactory.doGetBean()protectedTTdoGetBean(Stringname,ClassTrequiredType,Object[]args,booleantypeCheckOnly){// 1. 从一级缓存获取ObjectsharedInstancegetSingleton(beanName);if(sharedInstance!nullargsnull){beangetObjectForBeanInstance(sharedInstance,name,beanName,null);}else{// 2. 标记 A 正在创建beforeSingletonCreation(beanName);// 加入 singletonsCurrentlyInCreation// 3. 创建 BeansingletonObjectcreateBean(beanName,mbd,args);}}Step 2实例化 A放入三级缓存// AbstractAutowireCapableBeanFactory.doCreateBean()protectedObjectdoCreateBean(StringbeanName,RootBeanDefinitionmbd,Object[]args){// 1. 实例化 A调用构造方法此时 A 中 B 为 nullBeanWrapperinstanceWrappercreateBeanInstance(beanName,mbd,args);ObjectbeaninstanceWrapper.getWrappedInstance();// 2. 【关键】如果是单例、允许循环依赖、正在创建中放入三级缓存booleanearlySingletonExposure(mbd.isSingleton()this.allowCircularReferencesisSingletonCurrentlyInCreation(beanName));if(earlySingletonExposure){addSingletonFactory(beanName,()-{// 如果有 AOP 代理提前创建代理否则返回原始对象returngetEarlyBeanReference(beanName,mbd,bean);});}// 3. 属性填充populateBean—— 发现需要注入 BpopulateBean(beanName,mbd,instanceWrapper);// 4. 初始化initializeBeanexposedObjectinitializeBean(beanName,exposedObject,mbd);}Step 3创建 B发现需要 A// 在 populateBean 中发现 A 需要注入 B// 调用 getBean(b) → doGetBean(b)// B 的创建流程与 A 相同...// B 实例化后populateBean 发现需要注入 A// 调用 getBean(a) → doGetBean(a)Step 4从三级缓存获取 A 的半成品// DefaultSingletonBeanRegistry.getSingleton()protectedObjectgetSingleton(StringbeanName,booleanallowEarlyReference){// 1. 从一级缓存获取ObjectsingletonObjectthis.singletonObjects.get(beanName);if(singletonObjectnullisSingletonCurrentlyInCreation(beanName)){// 2. 一级缓存没有且正在创建中说明有循环依赖singletonObjectthis.earlySingletonObjects.get(beanName);if(singletonObjectnullallowEarlyReference){// 3. 从三级缓存获取 ObjectFactoryObjectFactory?singletonFactorythis.singletonFactories.get(beanName);if(singletonFactory!null){// 4. 调用 getObject() 获取 Bean或代理对象singletonObjectsingletonFactory.getObject();// 5. 提升到二级缓存this.earlySingletonObjects.put(beanName,singletonObject);this.singletonFactories.remove(beanName);}}}returnsingletonObject;}Step 5B 完成初始化A 完成初始化// B 获取到 A 的半成品后完成 populateBean 和 initializeBean// B 放入一级缓存addSingleton(b,b);// 回到 A 的创建流程B 已可用A 完成 populateBean 和 initializeBean// A 放入一级缓存addSingleton(a,a);// addSingleton 方法protectedvoidaddSingleton(StringbeanName,ObjectsingletonObject){synchronized(this.singletonObjects){this.singletonObjects.put(beanName,singletonObject);// 放入一级缓存this.singletonFactories.remove(beanName);// 移除三级缓存this.earlySingletonObjects.remove(beanName);// 移除二级缓存}}完整流程图getBean(a) ↓ doGetBean(a) ↓ getSingleton(a) → 一级缓存无开始创建 ↓ createBean(a) → doCreateBean(a) ↓ 实例化 A构造方法→ A 对象创建Bnull ↓ addSingletonFactory(a, ObjectFactory) → 放入三级缓存 ↓ populateBean(a) → 发现需要注入 B ↓ getBean(b) ↓ doGetBean(b) ↓ getSingleton(b) → 一级缓存无开始创建 ↓ createBean(b) → doCreateBean(b) ↓ 实例化 B构造方法→ B 对象创建Anull ↓ addSingletonFactory(b, ObjectFactory) → 放入三级缓存 ↓ populateBean(b) → 发现需要注入 A ↓ getBean(a) ↓ getSingleton(a, true) ↓ 一级缓存无 → A 正在创建中 ↓ earlySingletonObjects 无 ↓ singletonFactories 获取 ObjectFactory ↓ getObject() → 返回 A 的半成品或代理 ↓ 放入二级缓存移除三级缓存 ↓ 返回 A 的半成品 ↓ B 完成 populateBeanA 已注入 ↓ initializeBean(b) → B 完成初始化 ↓ addSingleton(b) → 放入一级缓存 ↓ 返回 B ↓ A 完成 populateBeanB 已注入 ↓ initializeBean(a) → A 完成初始化 ↓ addSingleton(a) → 放入一级缓存 ↓ 返回 A4. 为什么需要三级缓存二级缓存不够吗这是面试中最核心的问题需要从AOP 代理的角度理解4.1 如果只有两级缓存的问题假设只有一级缓存成品和二级缓存半成品A 实例化后如果需要 AOP 代理必须立即创建代理对象放入二级缓存但 AOP 代理的创建时机是在初始化后postProcessAfterInitialization此时 A 尚未完成依赖注入和初始化如果在实例化后立即创建代理后续的PostConstruct、InitializingBean等初始化逻辑会在代理对象上执行可能导致代理逻辑被覆盖或注入失败。4.2 三级缓存的精妙设计三级缓存存放的是ObjectFactory函数式接口而非直接的 Bean 实例addSingletonFactory(beanName,()-getEarlyBeanReference(beanName,mbd,bean));延迟创建代理只有当循环依赖发生、需要从三级缓存获取 Bean 时才调用ObjectFactory.getObject()。此时如果 A 需要 AOP 代理getEarlyBeanReference()会提前创建代理对象如果 A 不需要代理直接返回原始对象代理对象创建后提升到二级缓存避免重复创建。为什么提升到二级缓存如果循环依赖中有多个 Bean 依赖 A如B → A且C → A第一次从三级缓存获取 A 时创建代理并放入二级缓存后续再获取 A 时直接从二级缓存拿避免重复创建代理。4.3 三级 vs 二级缓存的本质区别维度二级缓存方案三级缓存方案存储内容直接存放 Bean 实例存放ObjectFactory代理创建时机实例化后立即创建按需延迟创建发生循环依赖时初始化顺序代理对象先创建初始化逻辑后执行原始对象先初始化代理按需创建灵活性低所有 Bean 都提前创建代理高只有循环依赖的 Bean 才提前创建代理性能差所有 Bean 都提前创建代理好按需创建代理5. 构造器循环依赖为什么无法解决构造器注入的循环依赖无法通过三级缓存解决根本原因在于创建时机的差异ServicepublicclassA{privatefinalBb;publicA(Bb){this.bb;}// 构造器注入}ServicepublicclassB{privatefinalAa;publicB(Aa){this.aa;}// 构造器注入}问题分析阶段Setter/字段注入构造器注入实例化无参构造创建对象有参构造创建对象参数必须已存在提前暴露实例化后即可暴露构造器执行时就需要 BB 尚未创建循环依赖解决✅ 可以❌ 无法提前暴露具体流程创建 A → 调用构造方法 A(B b) → 需要 B ↓ 创建 B → 调用构造方法 B(A a) → 需要 A ↓ A 尚未实例化完成无法从三级缓存获取 → 死循环解决方案方案实现方式说明改用 Setter/字段注入将final和构造器去掉改用Autowired最简单但牺牲不可变性Lazy 延迟注入Lazy Autowired private B b;注入代理对象延迟真正创建ObjectFactory 延迟获取Autowired private ObjectFactoryB bFactory;使用时才获取Lookup 方法Lookup protected abstract B getB();Spring 动态生成子类实现重构代码消除循环依赖最佳方案但成本最高Lazy 解决构造器循环依赖的原理ServicepublicclassA{privatefinalBb;publicA(LazyBb){// 注入的是 B 的代理对象this.bb;}}Spring 为 B 创建CGLIB 代理对象A 的构造器接收的是代理而非真实 B。代理对象可以立即创建不需要 B 的真实实例。当 A 初始化完成后B 再创建并注入到 A 中。6. 多例prototype循环依赖原型 Bean 不缓存每次getBean()都创建新实例因此无法通过三级缓存解决循环依赖Scope(prototype)ComponentpublicclassA{AutowiredprivateBb;}Scope(prototype)ComponentpublicclassB{AutowiredprivateAa;}报错BeanCurrentlyInCreationException解决方案使用ObjectFactory或Lookup延迟获取。7. 生产环境避坑指南7.1 启动时报BeanCurrentlyInCreationException检查是否是构造器循环依赖或多例循环依赖。查看异常堆栈中的 Bean 名称定位循环链。7.2 Async 与循环依赖Async代理的创建可能导致循环依赖检测失败。确保EnableAsync在EnableAspectJAutoProxy之后。7.3 AOP 代理与循环依赖的初始化顺序循环依赖中提前创建的代理对象其TargetSource指向的是半成品 Bean。初始化完成后代理对象的目标引用需要更新为完整的 Bean。7.4 使用DependsOn导致循环DependsOn(b)强制在创建 A 之前先创建 B如果 B 也DependsOn(a)会直接报错无法通过三级缓存解决。7.5 循环依赖是设计问题虽然 Spring 能解决 Setter 循环依赖但循环依赖通常意味着职责划分不清。最佳实践是通过重构消除循环依赖。8. 面试官追问与高分回答模板追问 1“Spring 是如何解决循环依赖的”低分回答“通过三级缓存在对象尚未完全初始化时提前暴露引用。”没有触及源码和 AOP 代理高分回答Spring 通过三级缓存解决单例 Bean 的 Setter/字段注入循环依赖缓存字段内容作用一级缓存singletonObjects成品 Bean对外提供二级缓存earlySingletonObjects半成品 Bean解决循环依赖避免重复创建代理三级缓存singletonFactoriesObjectFactory延迟创建代理对象完整流程A 实例化后构造方法执行完尚未注入依赖将ObjectFactory放入三级缓存A 属性填充时发现需要 B开始创建 BB 实例化后放入三级缓存属性填充时发现需要 A从三级缓存获取 A 的ObjectFactory调用getObject()获取 A 的半成品或代理对象提升到二级缓存B 完成初始化放入一级缓存A 继续初始化放入一级缓存。关键是三级缓存存放ObjectFactory而非直接实例实现了代理对象的延迟创建——只有发生循环依赖时才提前创建代理避免所有 Bean 都提前代理的性能损耗。追问 2“为什么需要三级缓存二级缓存不够吗”高分回答二级缓存不够核心原因是AOP 代理的创建时机问题。如果只有两级缓存成品 半成品A 实例化后必须立即创建代理对象放入二级缓存。但 AOP 代理的正常创建时机是在初始化后postProcessAfterInitialization此时PostConstruct、InitializingBean等初始化逻辑尚未执行。如果提前创建代理初始化逻辑会在代理对象上执行可能覆盖代理逻辑所有 Bean 都提前创建代理性能损耗大。三级缓存存放ObjectFactory实现了按需延迟创建只有当发生循环依赖、需要从缓存获取 Bean 时才调用getObject()创建代理。这样既保证了循环依赖时能获取到代理对象又避免了不必要的提前代理。二级缓存earlySingletonObjects的作用是避免重复创建代理如果多个 Bean 循环依赖 A第一次从三级缓存创建代理后放入二级缓存后续直接从二级缓存获取。追问 3“构造器循环依赖为什么无法解决”高分回答构造器循环依赖无法解决的根本原因是实例化和依赖注入的时机重叠。Setter/字段注入的流程是先无参构造实例化 → 提前暴露到三级缓存 → 再注入依赖。此时 Bean 已经存在可以被其他 Bean 引用。构造器注入的流程是调用构造方法时就需要传入依赖对象。创建 A 的构造方法A(B b)时B 尚未创建创建 B 的构造方法B(A a)时A 尚未实例化完成。两者互相等待形成死锁。解决方案改用 Setter/字段注入最简单但牺牲不可变性Lazy延迟注入public A(Lazy B b)注入 B 的代理对象延迟真正创建ObjectFactory延迟获取使用时才getObject()重构代码消除循环依赖最佳方案。追问 4“Lazy 是怎么解决循环依赖的”高分回答Lazy解决循环依赖的原理是延迟注入代理对象。以构造器循环依赖A(B)→B(A)为例publicA(LazyBb){this.bb;}Spring 创建 A 时发现构造器参数 B 标注了LazySpring 不为 B 创建真实实例而是创建 B 的CGLIB 代理对象A 的构造器接收代理对象A 实例化完成并提前暴露到三级缓存创建 B 时从三级缓存获取 A 的半成品注入B 初始化完成A 初始化完成代理对象中的目标引用更新为完整的 B。关键点代理对象可以立即创建不需要依赖的真实实例。Lazy本质上是用空间换时间用代理对象打破循环等待。追问 5“Spring Boot 中怎么检测和避免循环依赖”高分回答Spring Boot 2.6 默认禁止循环依赖spring.main.allow-circular-referencesfalse启动时检测到循环依赖会直接报错。检测方法查看启动异常BeanCurrentlyInCreationException堆栈会显示循环链使用 IDE 插件如 IntelliJ 的 Spring 依赖分析可视化 Bean 依赖图代码审查检查Autowired和构造器注入的依赖关系。避免策略分层架构Controller → Service → DAO同层不依赖避免循环事件驱动使用ApplicationEventPublisher解耦依赖抽象公共逻辑将双向依赖的公共部分抽取到第三个 Bean接口隔离通过接口降低耦合但注意接口之间也可能循环。追问 6“如果 A 需要 AOP 代理循环依赖中提前暴露的是代理对象还是原始对象”高分回答循环依赖中提前暴露的是代理对象如果 A 需要 AOP 增强。具体流程A 实例化后三级缓存中放入ObjectFactoryB 创建时需要 A从三级缓存调用ObjectFactory.getObject()getObject()内部调用getEarlyBeanReference(beanName, mbd, bean)getEarlyBeanReference()遍历SmartInstantiationAwareBeanPostProcessorAbstractAutoProxyCreator判断 A 需要代理提前创建代理对象代理对象返回给 BB 注入的是 A 的代理A 初始化完成后代理对象的TargetSource指向完整的 A。这意味着B 中注入的 A 从一开始就是代理对象而非先注入原始对象再替换。这是三级缓存设计的关键——确保循环依赖中注入的是一致的代理对象。9. 方案选型速查表循环依赖场景是否支持解决方案推荐度单例 Setter/字段注入✅三级缓存自动解决⭐⭐⭐⭐⭐单例 构造器注入❌改用 Setter、Lazy、ObjectFactory、重构⭐⭐⭐⭐单例 DependsOn❌消除 DependsOn 循环⭐⭐⭐⭐⭐原型prototype❌ObjectFactory、Lookup、重构⭐⭐⭐多例 构造器❌重构消除循环⭐⭐⭐⭐⭐AOP 代理 循环依赖✅三级缓存自动处理代理提前暴露⭐⭐⭐⭐⭐面试官想要的满分总结Spring 解决循环依赖的核心设计是三级缓存 提前暴露但这只适用于单例 Setter/字段注入的场景。理解三级缓存必须抓住三个关键点三级缓存存放ObjectFactory而非实例实现了 AOP 代理的延迟创建。只有发生循环依赖时才提前创建代理避免所有 Bean 都提前代理的性能损耗。二级缓存earlySingletonObjects则避免重复创建代理。构造器循环依赖无法解决因为构造器执行时 Bean 尚未实例化无法提前暴露。解决方案包括Lazy注入代理对象、ObjectFactory延迟获取、或重构消除循环。循环依赖是设计问题虽然 Spring 能解决 Setter 循环依赖但通常意味着职责划分不清。Spring Boot 2.6 默认禁止循环依赖倒逼更好的设计。生产中最容易踩的坑是构造器注入的循环依赖启动失败和忘记Lazy导致的问题。记住三级缓存是 Spring 的兜底机制不是鼓励循环依赖的许可证。优秀的架构应该通过分层设计和事件驱动消除循环依赖而非依赖框架的容错能力。觉得对您有帮助麻烦点点关注啦您的关注是我创作的最大动力~