资讯动态

工厂方法循环依赖:Spring三级缓存为何失效及解决方案

发布时间:2026/10/8 20:34:58 来源:尧图企业网站定制
年前接手一个老项目启动时直接给我甩了一屏红BeanCurrentlyInCreationException: Error creating bean with name orderQueryService。当时第一反应是又踩循环依赖了毕竟Spring Boot 2.6之后默认就禁止循环引用多半是allow-circular-references没开。但仔细看堆栈却不对劲——异常指向的不是字段注入而是配置类里的一个Bean工厂方法参数另一个 Service 类型传不进来。这就不是打开一个开关能糊弄过去的了。这篇文章专门聊工厂方法创建Bean时的循环依赖这个点为什么Bean方法、静态工厂方法这些工厂形态在遇到循环依赖时Spring 的三级缓存基本使不上劲以及 Spring Boot 2.x/3.x 下这类问题应该怎么排查、怎么解。1. 先看现场工厂方法循环依赖的报错长什么样1.1 一段最典型的工厂方法循环依赖代码先还原一下我当时遇到的场景。为了演示把业务类简化成两个 Service彼此需要引用对方Configuration public class BizConfig { Bean public OrderQueryService orderQueryService(OrderExecuteService executeService) { return new OrderQueryService(executeService); } Bean public OrderExecuteService orderExecuteService(OrderQueryService queryService) { return new OrderExecuteService(queryService); } }OrderQueryService构造器接收OrderExecuteServiceOrderExecuteService构造器接收OrderQueryService。两个Bean方法的参数都是对方容器一启动就形成闭环。这种写法本质上就是工厂方法参数级循环依赖。启动后核心异常是这样的UnsatisfiedDependencyException: Error creating bean with name orderQueryService: Unsatisfied dependency expressed through method orderQueryService parameter 0; nested exception is BeanCurrentlyInCreationException: Error creating bean with name orderQueryService: Requested bean is currently in creation: Is there an unresolvable circular reference?注意这里的措辞——expressed through method orderQueryService parameter 0。这一句就是关键线索依赖是通过工厂方法的参数传递的而不是通过字段或 setter。这说明 Spring 在处理Bean方法的参数时遇到了创建期的循环依赖。1.2 报错信息里的关键线索同样是循环依赖不同注入方式的报错信息是不同的读法也不一样。如果是普通字段注入循环Spring Boot 2.6 之前通常能看到这样的提示The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | orderQueryService (field orderExecuteService) ↑ ↓ | orderExecuteService (field orderQueryService) └─────┘如果是构造器注入或工厂方法参数注入报错往往不是BeanCreationNotAllowedException而是UnsatisfiedDependencyException包裹的BeanCurrentlyInCreationException。为什么会有一个CurrentlyInCreation因为 Spring 在创建orderQueryService的过程中发现它正在创建中——同一线程带着同一个 Bean 的名字又回到了创建入口这种递归式的回头在创建期是不允许的。遇到BeanCurrentlyInCreationException的时候第一件事不是开allow-circular-references而是先分辨这个循环是发生在实例化/创建阶段还是发生在属性填充阶段。后者才有概率被三级缓存救回来前者基本没救。1.3 为什么这不是调大 allow-circular-references 就能解决的很多人在 Boot 2.6 之后遇到循环依赖第一反应就是配置spring.main.allow-circular-referencestrue这个开关对普通 setter/字段注入循环是有效的因为它允许容器在属性填充阶段提前暴露早期引用。但工厂方法参数循环不一样Bean方法的参数解析发生在实例化阶段这个阶段在 Spring 的 Bean 创建流程里排得很靠前早于早期引用的暴露。开关打开后三级缓存虽然允许被注册但工厂方法解析参数的时机根本轮不到走三级缓存。所以结论很明确工厂方法创建 Bean 时的循环依赖不是配置开关能解决的必须换路子。后面几节我会把底层原因彻底拆开。2. 三级缓存为什么救不了工厂方法执行顺序才是本质要搞清楚这个问题不能只在日志层面打转。Spring 解决循环依赖的机制是三级缓存 早期引用暴露而工厂方法的问题恰恰卡在了这个机制生效之前。2.1 三级缓存各管什么所有单例 Bean 都会进DefaultSingletonBeanRegistry的缓存体系三级缓存分别是缓存名称存的是什么一级singletonObjects创建完成的成品 Bean大家平时 getBean 拿到的就是它二级earlySingletonObjects提前暴露的半成品 Bean原始对象或代理循环依赖中被对方提前引用的对象三级singletonFactoriesObjectFactory工厂真正需要提前暴露时才调用它生成早期引用关键在第三级。它不是直接存对象而是存一个懒工厂addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean))。只有当另一个 Bean 在创建过程中反过来需要当前 Bean 时才会触发这个工厂生成一个早期引用放到二级缓存里再注入给对面的 Bean。2.2 doCreateBean 里的三步执行顺序AbstractAutowireCapableBeanFactory#doCreateBean是单例 Bean 创建的核心方法整个流程可以简化成三步// 第一步实例化 Bean构造器、工厂方法都在这里 Object bean createBeanInstance(beanName, mbd, args); // 第二步允许循环引用时注册三级缓存提前暴露早期引用 boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); } // 第三步填充属性字段注入、setter 注入都在这里 populateBean(beanName, mbd, instanceWrapper);注意顺序实例化 → 注册三级缓存 → 填充属性。三级缓存的注册发生在实例化完成之后、属性填充之前。也就是说凡是需要在属性填充阶段回环过来的依赖Spring 都能通过三级缓存拿到当前 Bean 的早期引用凡是需要在实例化阶段就拿到依赖的当前 Bean 还没走到注册三级缓存这一步自然拿不到早期引用。这就是 Spring 能解决 setter/字段注入循环依赖、却解决不了构造器注入循环依赖的底层原因。2.3 工厂方法依赖解析发生在哪一环那工厂方法创建 Bean 属于哪一步答案很直接属于第一步createBeanInstance。Bean方法在 Spring 内部其实是被封装成一个RootBeanDefinitionbeanClass是配置类factoryMethodName是方法名。创建这个 Bean 时容器会走到ConstructorResolver#instantiateUsingFactoryMethod它要做的事情和构造器注入非常像——解析工厂方法的参数// ConstructorResolver#instantiateUsingFactoryMethod 中会解析构造参数 ConstructorArgumentValues resolvedValues new ConstructorArgumentValues(); resolvedValues.addGenericArgumentValue(...); // 解析参数时可能触发其他 Bean 的加载 Object[] args resolveConstructorArguments(beanName, mbd, bw, constructorToUse, resolvedValues);resolveConstructorArguments最终会调用beanFactory.resolveDependency(...)去解析参数类型对应的 Bean。在解析orderQueryService方法的参数OrderExecuteService时容器就需要先创建orderExecuteService创建orderExecuteService时又要解析它的参数OrderQueryService此时orderQueryService正在创建中但它的三级缓存还没注册因为还没从createBeanInstance走出来于是getSingleton拿不到最终抛出那个BeanCurrentlyInCreationException。所以工厂方法的参数依赖在创建链路上的位置和构造器参数一模一样它发生在三级缓存生效之前。容器没有机会提前暴露早期引用死路一条。2.4 getEarlyBeanReference 与早期代理顺手补一个知识点为什么三级缓存不干脆存原始 Bean 而是要包一层 ObjectFactory因为getEarlyBeanReference不只是返回原始对象它还会让SmartInstantiationAwareBeanPostProcessor处理早期引用最常见的就是生成 AOP 代理。比如orderQueryService本身需要被 AOP 切面代理在循环依赖里被orderExecuteService提前引用到的那个对象必须是最终那个代理对象而不是代理之前的裸对象。三级缓存的设计保证了需要提前暴露时才去生成代理生成一次放到二级缓存保证全局引用一致。如果直接用二级缓存存裸对象会导致两个问题一是代理没生效B 拿到的是 A 的原始对象后面 A 被代理了两边引用不一致二是无法保证只暴露一次可能生成多个代理对象。理解这个机制再看工厂方法循环依赖就更容易明白——工厂方法阶段连暴露这个动作都还没发生后面这些精妙设计对它是无效的。3. 三种工厂形态循环依赖结果各不相同工厂方法在 Spring 里其实有好几种落地形态它们在循环依赖面前的表现不完全一样平时很多人混为一谈。我把常见的三种拆开来讲每种给结论。3.1 Bean 方法参数注入最常见的形态Bean方法通过参数声明依赖是 Spring Boot 项目里最常见的装配方式。前面已经说过它的参数由ConstructorResolver解析发生在实例化阶段遇到循环依赖必死。这里有个容易忽略的细节Bean方法的 bean definition 里autowireMode会被设为AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR。换句话说Spring 根本就是把Bean方法当作一个构造函数来看待的。所以它在循环依赖问题上的行为和构造器注入完全一致——这也是为什么很多人把Bean参数循环跟构造器循环归为一类本质上就是同一个问题。3.2 Bean 方法体内调用另一个 Bean 方法同样踩雷有人会想那我不用参数注入改成方法体内手动调用另一个Bean方法行不行Configuration public class BizConfig { Bean public OrderQueryService orderQueryService() { return new OrderQueryService(orderExecuteService()); } Bean public OrderExecuteService orderExecuteService() { return new OrderExecuteService(orderQueryService()); } }答案是要分情况。如果BizConfig是被 CGLIB 代理的完整Configuration默认proxyBeanMethods true那么配置类里的Bean方法会被代理拦截方法体内调用orderExecuteService()时并不会直接执行原始方法体而是通过容器getBean(orderExecuteService)获取。这本质上还是走 Spring 容器两个方法互相调用创建期依赖依然发生在createBeanInstance阶段循环依赖照样报错。如果你把配置类改成proxyBeanMethods falseConfiguration(proxyBeanMethods false) public class BizConfig { ... }那就完全是另一个故事了——方法体内的Bean方法调用变成普通 Java 方法调用不再经过容器。此时两个方法互相调用会直接在 Java 栈上无限递归最终抛StackOverflowError而不是 Spring 的循环依赖异常。业务上还会出现单例失效每次调用Bean方法都执行原始方法体返回全新实例。所以这种方法体内互相调用的做法在完整配置类下解决不了循环依赖在非代理配置类下则会变成一个更原始的问题。出路还是别让两个Bean方法互相引用。3.3 静态工厂方法BeanDefinition 注册本质一致还有一种工厂方法不是写在Configuration里而是通过BeanDefinition指定静态工厂方法。比如BeanDefinitionBuilder.genericBeanDefinition(OrderFactory.class) .setFactoryMethodName(createQueryService) .addConstructorArgValue(orderExecuteService) // 依赖另一个 Bean .getBeanDefinition();或者 XML 时代常见的写法bean idorderQueryService classcom.example.OrderFactory factory-methodcreateQueryService constructor-arg reforderExecuteService/ /bean只要工厂方法需要接收参数、参数又依赖回当前的 Bean循环依赖就走不通。因为静态工厂方法的参数同样由ConstructorResolver解析同样发生在createBeanInstance阶段。不要以为换成静态工厂方法就能躲过三级缓存的边界问题它和Bean方法参数在创建链路上的位置一模一样。不过有一点可以区分如果静态工厂方法不需要任何参数只是单纯用工厂方法替代构造器来拿到一个 Bean那它根本不会产生参数级循环依赖。循环依赖必须要有依赖存在没有依赖引用就没有环。3.4 附带提一下 FactoryBean它也不是避风港FactoryBean是另一种工厂形态很多人以为它够特殊可以绕开循环依赖。实际上它也不是绝对安全。FactoryBean本身是一个单例 Bean创建它的时候走的还是doCreateBean流程。如果FactoryBean的成员变量里注入了另一个 Bean A而 A 又依赖FactoryBean生成的目标类型那么在创建FactoryBean的过程中A 的创建会反向触达FactoryBean生成的目标 Bean。此时FactoryBean可能还在singletonsCurrentlyInCreation名单里getBean(targetName)依然可能触发创建期死锁。即使FactoryBean自身没有属性循环只是在getObject()内部去容器拿依赖你也要非常小心getObject()的调用时机、目标 Bean 的缓存位置都和普通Bean方法不同。它可以作为解决问题的一种手段但不是万能钥匙。3.5 行为对比表把几种形态放在一起看结论更清晰创建方式依赖解析时机参数/依赖回环时三级缓存能否救Bean 方法参数注入实例化阶段工厂方法调用前未注册三级缓存不能救Bean 方法体内调用另一 BeanproxyBeanMethodstrue实例化阶段工厂方法执行中未注册三级缓存不能救Bean 方法体内调用另一 BeanproxyBeanMethodsfalse无容器介入Java 栈递归超出 Spring 范围静态工厂方法 构造参数实例化阶段工厂方法调用前未注册三级缓存不能救FactoryBean.getObject() 内获取依赖FactoryBean 实例化后目标 Bean 获取时视是否反向依赖而定有限场景可用setter / 字段注入属性填充阶段已注册三级缓存已注册三级缓存默认可救Boot 2.6 需开开关构造器注入实例化阶段构造器调用前未注册三级缓存不能救最核心的分界线就是一句大白话依赖发生在实例化前/中还是属性填充时。前者是死结后者才有的谈。4. 从应急到根治工厂方法循环依赖的解决路径前面把原理和形态讲透了剩下的就是动手解决。我的建议是先做应急让项目跑起来再做根治让依赖关系健康。4.1 Lazy把创建期依赖推迟到使用期最快速的应急方案是在导致循环的Bean方法参数上加LazyConfiguration public class BizConfig { Bean public OrderQueryService orderQueryService(Lazy OrderExecuteService executeService) { return new OrderQueryService(executeService); } Bean public OrderExecuteService orderExecuteService(OrderQueryService queryService) { return new OrderExecuteService(queryService); } }Lazy在参数上生效时Spring 不会在解析工厂方法参数时真正去创建OrderExecuteService而是注入一个懒加载代理对象。代理里面暂时没有真实引用等到OrderQueryService真正调用executeService的某个方法时才会触发getBean(orderExecuteService)。这时候orderQueryService已经创建完成循环链条被打断容器可以正常启动。用这个方案有几个坑要提醒懒代理和真实对象不是同一个引用做比较、getClass()判断、强转具体类时都可能出问题。被代理的类尽量不要有final方法否则代理无法正常拦截。调用链如果很早就触发代理方法那么创建期循环只是被推到了运行时仍然可能变成运行时依赖报错。所以Lazy对我来说是应急方案不是根治方案。它适合快速解障但依赖关系本身依然是环只是藏到了运行时。4.2 ObjectProvider更可控的延迟句柄比Lazy稍微清醒一点的方案是ObjectProvider它是 Spring 5.1 开始大力推荐的依赖选择器Configuration public class BizConfig { Bean public OrderQueryService orderQueryService(ObjectProviderOrderExecuteService executeServiceProvider) { return new OrderQueryService(executeServiceProvider); } Bean public OrderExecuteService orderExecuteService(ObjectProviderOrderQueryService queryServiceProvider) { return new OrderExecuteService(queryServiceProvider); } }ObjectProvider本身是一个句柄对象注入它不需要立刻解析具体依赖你在代码里通过getObject()、getIfAvailable()、getIfUnique()在合适的时机获取实际 Bean 即可。相比Lazy代理它有几个优势可以通过getIfAvailable()做存在性判断应对可能没有 Bean的场景。可以做多候选筛选比如ifUnique。解析时机完全由业务代码控制不会自动触发代理方法语义更直观。代价是业务类里多了一个ObjectProvider包装调用处要显式getObject()代码可读性稍微打折。4.3 打破环路的架构调整真正想根治循环依赖调整结构才是最健康的方式。利落地剪断一条依赖永远比在环上做手脚更省心。常见做法是单向化让其中一个 Bean 不再直接依赖另一个。比如OrderQueryService需要OrderExecuteService的能力可以反过来由OrderExecuteService持有OrderQueryService而OrderQueryService通过事件、回调、或者把依赖下沉到一个公共子模块来解耦。另一种思路是中间层把两个 Service 共同依赖的逻辑抽到一个新的 Service 或组件里两边都只依赖中间层不再互依。代价是多一个类代码结构会清爽很多。如果只是局部需要对方的某个能力也不妨考虑方法参数传递把一个 Bean 的方法编写成接收外部传入的协作对象而不是在字段/构造器里持有对方。依赖从长期持有变成用时即取环路自然消失。4.4 总开关 allow-circular-references 到底管什么回到 Spring Boot 2.6 之后的默认行为。配置里最常见的开关spring.main.allow-circular-referencestrue它的作用是设置AbstractAutowireCapableBeanFactory.allowCircularReferences决定doCreateBean里earlySingletonExposure是否成立。如果开关是 false那么即使属性填充阶段的循环依赖也不会注册三级缓存直接报循环依赖异常。但回到我们文章的标题场景——工厂方法创建 Bean 时的参数循环依赖——这个开关根本来不及起作用。因为工厂方法解析参数发生在createBeanInstance而allowCircularReferences影响的是createBeanInstance之后的二级设定。开关打开只是让属性填充阶段的循环依赖有了被三级缓存处理的机会它救不了实例化阶段就回环的死结。所以别再一遇到循环依赖就开这个开关了。先判断你的循环发生在哪个阶段再决定要不要用开关、怎么用Lazy或ObjectProvider。4.5 一条实用的排查步骤总结一套我自己常用的排查链路遇到 Spring Boot 循环依赖异常时可以按顺序走看异常类型UnsatisfiedDependencyExceptionBeanCurrentlyInCreationException大概率是构造器/工厂方法参数循环BeanCreationNotAllowedException通常是容器关闭或显式循环引用限制。读堆栈里的方法签名expressed through method xxx parameter 0直接告诉你哪个Bean方法的参数在回环。画出依赖图把涉及循环的 Bean 和依赖方向记录下来找到哪个依赖是创建期依赖构造器参数、工厂方法参数、静态工厂方法参数哪个是属性期依赖字段/setter。优先剪断创建期依赖给工厂方法参数加Lazy或换成ObjectProvider先让项目启动起来。评估是否根治启动成功后回头审视两个 Bean 的职责边界。如果长期互相调用考虑抽取中间层、回调或事件。最后才碰全局开关除非你确认循环只存在于属性填充阶段而且项目就是想让循环存在否则别让spring.main.allow-circular-referencestrue成为默认配置。我自己实际操作的经验是90% 的工厂方法循环依赖在画完依赖图之后都能找到一个看似便利、实则多余的反向依赖。把这个反向依赖剪掉比任何技术技巧都省心。Lazy和ObjectProvider是我用来救火的工具但每次用完我都要追问一句这个环是不是本来就不该存在如果答案是该那我就把它拆了而不是留着隐患。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑