Spring PROTOTYPE作用域Bean循环依赖问题及解决方案咨询
你遇到的这个问题是Spring的一个明确限制:Spring原生不支持解决原型(PROTOTYPE)作用域Bean的循环依赖,这也是为什么你的单例Bean能正常运行,但改成原型后就抛出循环依赖异常的原因。你看到的错误提示也印证了这一点:
描述:
应用上下文部分Bean的依赖形成循环:
demoApplication (field private com.example.demo.beans.BeanA com.example.demo.DemoApplication.beanA)
为什么单例可以,原型不行?
这里的核心差异在于Spring对不同作用域Bean的缓存机制:
- 单例Bean:Spring通过三级缓存(
singletonObjects、earlySingletonObjects、singletonFactories)实现循环依赖解决——在Bean还未完全初始化时,就提前暴露它的代理对象或原始引用,让依赖它的Bean先拿到实例,后续再完成初始化流程。 - 原型Bean:每次请求都会创建全新的实例,Spring不会将原型Bean存入任何缓存。当创建BeanA的原型实例时,需要注入BeanB的原型实例;创建BeanB的原型实例时,又需要注入BeanA的原型实例,直接陷入无限循环的创建链条,最终触发循环依赖异常。
可行的解决方案
1. 使用@Lazy延迟初始化
在其中一个Bean的依赖注入处添加@Lazy注解,让依赖的Bean在真正被使用时才创建,而不是在当前Bean初始化时就触发创建,从而打破循环链条:
@Service @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class BeanA { private BeanB beanB; @Autowired public void setBeanB(@Lazy BeanB beanB) { this.beanB = beanB; } // 其他方法保持不变 }
此时创建BeanA的原型实例时,注入的是一个代理对象,只有当你调用beanB的方法时,Spring才会去创建BeanB的原型实例,而这时BeanA已经完成初始化,不会触发循环。
2. 使用ObjectProvider延迟获取实例
通过Spring的ObjectProvider(或JSR-330的Provider)来封装依赖,每次调用getObject()才会生成新的原型Bean实例,避免初始化时的循环创建:
import org.springframework.beans.factory.ObjectProvider; @Service @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class BeanA { private ObjectProvider<BeanB> beanBProvider; @Autowired public void setBeanBProvider(ObjectProvider<BeanB> beanBProvider) { this.beanBProvider = beanBProvider; } public void printBeanABeanB() { // 真正使用时才获取BeanB的原型实例 BeanB beanB = beanBProvider.getObject(); System.out.println(getBeanName() + " - " + beanB.getBeanName()); } // 其他方法保持不变 }
这种方式既解决了循环依赖,又完美保留了原型Bean每次获取新实例的特性。
3. 通过ApplicationContext手动获取Bean
在Bean中注入ApplicationContext,在需要使用依赖Bean时手动调用getBean()方法获取实例,同样是延迟创建来避免循环:
import org.springframework.context.ApplicationContext; @Service @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class BeanA { private ApplicationContext applicationContext; @Autowired public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } public void printBeanABeanB() { BeanB beanB = applicationContext.getBean(BeanB.class); System.out.println(getBeanName() + " - " + beanB.getBeanName()); } // 其他方法保持不变 }
注意:这种方式会增加代码对Spring上下文的耦合度,尽量只在临时场景使用。
4. 重构设计,从根源打破循环依赖
最优雅的长期解决方案是重构代码结构,移除循环依赖:
- 提取两个Bean的公共逻辑到一个新的独立Bean(比如
CommonService),让BeanA和BeanB都依赖这个新Bean,而非互相依赖。 - 调整交互方式,比如通过事件驱动、回调接口等方式,让两个Bean不再直接持有对方的引用。
总结
Spring原生不支持原型Bean的循环依赖,但可以通过延迟初始化、Provider封装、手动获取实例等方式绕过这个限制;如果项目允许,重构设计打破循环依赖是最推荐的方案,能让代码结构更清晰、耦合度更低。
内容的提问来源于stack exchange,提问作者Chamly Idunil

