ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring循环依赖问题解析与解决方案

Spring循环依赖问题解析与解决方案 1. 问题背景当Spring遇上循环依赖那天下午我正在调试一个订单处理模块系统突然抛出BeanCurrentlyInCreationException异常。控制台醒目的红色日志显示Requested bean is currently in creation: Is there an unsolved circular reference?。这是一个典型的Spring循环依赖场景但这次的排查过程远比想象中复杂——因为涉及到了Async异步调用和自定义的AgentService。循环依赖就像两个互相等待的快递员A包裹需要B先签收才能派送而B包裹又要求A先确认收货。在Spring容器启动时这种互相依赖的关系会导致IoC容器无法完成bean的初始化闭环。虽然Spring通过三级缓存机制解决了部分循环依赖问题但当遇到特殊场景时这套机制就会失效。2. 循环依赖的产生条件分析2.1 典型循环依赖场景先看一个最简单的循环依赖例子Service class ServiceA { Autowired private ServiceB serviceB; } Service class ServiceB { Autowired private ServiceA serviceA; }Spring通过三级缓存singletonObjects、earlySingletonObjects、singletonFactories可以处理这种简单情况。但当引入代理、AOP或特殊注解时情况会变得复杂。2.2 本次复杂场景的特殊性我的项目结构是这样的Service class OrderService { Autowired private AgentService agentService; Async public void asyncProcess() { // 异步处理逻辑 } } Service class AgentService { Autowired private OrderService orderService; public void dispatch() { orderService.asyncProcess(); // 这里调用异步方法 } }问题特殊在Async会创建代理对象代理对象的初始化时机与普通bean不同Spring的三级缓存对代理对象的处理有特殊逻辑3. 深度排查过程实录3.1 异常堆栈分析首先查看完整的异常堆栈org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name orderService: Bean with name orderService has been injected into other beans [...] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching.关键信息是has eventually been wrapped说明Spring检测到bean被包装代理后出现了问题。3.2 Spring三级缓存原理回顾先理解Spring解决循环依赖的核心机制第一级缓存singletonObjects存放完全初始化好的bean第二级缓存earlySingletonObjects存放早期引用未完成属性注入第三级缓存singletonFactories存放bean工厂对象对于普通bean的循环依赖Spring的处理流程是创建A对象半成品未注入属性将A工厂放入三级缓存开始注入A的依赖发现需要B创建B对象同样将B工厂放入三级缓存注入B的依赖时从三级缓存拿到A的早期引用B完成初始化后A继续完成初始化3.3 代理对象导致的差异当bean需要被代理时如使用Async情况会发生变化代理对象的创建发生在AbstractAutoProxyCreator后置处理器中最终暴露给其他bean的应该是代理对象而非原始对象如果两个互相依赖的bean都需要代理就可能出现代理包装时机问题在我们的案例中OrderService需要被代理因为有AsyncAgentService在初始化时需要注入OrderService但此时OrderService的代理对象还未创建完成导致注入的是原始对象而非代理对象4. 解决方案与验证4.1 方案一重构代码消除循环依赖推荐最彻底的解决方案是重构代码结构Service class OrderService { // 移除对AgentService的直接依赖 public void asyncProcess() { // 异步处理逻辑 } } Service class AgentService { // 通过事件或消息队列解耦 EventListener public void handleOrderEvent(OrderEvent event) { // 处理订单事件 } }优点完全消除循环依赖符合单一职责原则后期维护成本低4.2 方案二使用Setter注入Lazy如果暂时无法重构可以使用延迟加载Service class OrderService { private AgentService agentService; Autowired public void setAgentService(Lazy AgentService agentService) { this.agentService agentService; } }原理Lazy会创建一个代理对象暂时代替实际bean只有当真正调用方法时才会触发实际bean的初始化打破了初始化时的循环链条4.3 方案三调整代理创建顺序通过实现BeanPostProcessor手动控制代理创建public class CustomProxyProcessor implements BeanPostProcessor { Override public Object postProcessAfterInitialization(Object bean, String beanName) { if(bean instanceof OrderService) { // 自定义代理创建逻辑 return Proxy.newProxyInstance(...); } return bean; } }注意事项需要精确控制代理创建时机可能影响其他AOP功能维护成本较高5. 经验总结与避坑指南5.1 循环依赖的预防措施代码设计阶段遵循依赖倒置原则DIP使用接口抽象降低耦合度考虑使用事件驱动架构开发规范层面在团队中明确禁止双向依赖使用ArchUnit等工具进行架构约束定期进行代码评审5.2 排查循环依赖的技巧日志分析开启Spring调试日志logging.level.org.springframeworkDEBUG关注Creating instance of bean和Exposing bean as factory日志工具使用# 使用Spring Boot Actuator查看bean依赖 GET /actuator/beans可视化工具使用IDEA的UML插件生成类图Spring Tools Suite的依赖分析功能5.3 特殊注解的注意事项使用以下注解时需要特别注意循环依赖风险注解风险点解决方案Async创建代理对象使用Lazy延迟注入Transactional可能影响代理顺序避免在循环依赖链中使用Cacheable缓存代理的创建时机考虑显式缓存调用Scope(prototype)不适用三级缓存重构为单例模式6. Spring三级缓存的底层实现6.1 DefaultSingletonBeanRegistry源码解析关键代码片段protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 1. 检查一级缓存 Object singletonObject this.singletonObjects.get(beanName); if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 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; }6.2 代理对象的特殊处理AbstractAutowireCapableBeanFactory中的关键方法protected Object doCreateBean(..., boolean earlySingletonExposure) { // 1. 创建原始bean实例 Object beanInstance createBeanInstance(beanName, mbd, args); if (earlySingletonExposure) { // 2. 添加到三级缓存重要 addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, beanInstance)); } // 3. 属性注入可能触发循环依赖 populateBean(beanName, mbd, instanceWrapper); // 4. 初始化后处理包括AOP代理创建 exposedObject initializeBean(beanName, exposedObject, mbd); }对于需要代理的beangetEarlyBeanReference()会提前返回代理对象这是解决普通循环依赖的关键。但当两个bean都需要代理时就可能出现问题。7. 高级解决方案探讨7.1 使用ObjectProvider延迟注入Service class OrderService { private final ObjectProviderAgentService agentServiceProvider; Autowired public OrderService(ObjectProviderAgentService agentServiceProvider) { this.agentServiceProvider agentServiceProvider; } public void execute() { AgentService agentService agentServiceProvider.getIfUnique(); // 使用agentService } }优势完全控制依赖获取时机避免启动时循环依赖支持条件性依赖获取7.2 基于事件的总线模式Component class OrderEventPublisher { Autowired private ApplicationEventPublisher eventPublisher; public void publishOrderEvent(OrderEvent event) { eventPublisher.publishEvent(event); } } Component class AgentService { EventListener public void handleOrderEvent(OrderEvent event) { // 处理事件 } }这种模式彻底解耦了服务间的直接依赖。7.3 使用Spring的DependsOnService DependsOn(agentService) class OrderService { // ... } Service class AgentService { // ... }强制控制bean的初始化顺序但要注意只是掩盖而非真正解决问题可能导致其他初始化问题不建议作为长期方案8. 性能影响与最佳实践循环依赖对系统的影响不仅体现在启动时还会带来运行时问题启动时间循环依赖会使bean初始化过程复杂化增加启动时间内存占用早期对象会同时存在于多个缓存中增加内存压力维护成本循环依赖的代码更难理解和修改推荐的最佳实践分层架构严格遵循Controller → Service → Repository的调用方向禁止同层之间的双向依赖依赖注入原则graph LR A[高层模块] --|依赖| B[抽象接口] C[低层实现] --|实现| B测试验证使用集成测试验证启动顺序编写ArchUnit测试约束架构规则定期进行依赖关系审查9. 类似问题的扩展思考这次排查经历让我联想到其他类似的Spring陷阱构造器注入循环依赖Spring无法处理构造器注入的循环依赖必须使用setter注入或字段注入PostConstruct方法中的依赖调用Service class ServiceA { Autowired private ServiceB serviceB; PostConstruct public void init() { serviceB.doSomething(); // 可能导致NPE } }多线程环境下的代理对象异步方法中使用的代理对象需要特别注意线程安全建议使用ConcurrentHashMap缓存代理实例10. 终极解决方案架构层面的思考经过这次深度排查我认为最根本的解决方案是明确模块边界使用Java 9的模块系统或通过Maven/Gradle模块划分依赖方向控制定义清晰的依赖规则如基础设施层→领域层→应用层→表现层使用依赖检查工具如JDepend强制执行领域驱动设计通过限界上下文Bounded Context划分服务边界使用防腐层ACL处理跨上下文交互对于大型项目建议采用如下的依赖关系矩阵模块允许依赖的模块webservice, commonservicerepository, domainrepositorydomain, commondomaincommoncommon无这种严格的依赖管控可以彻底避免循环依赖问题。
返回列表