ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring三级缓存机制深度解析:如何解决循环依赖与AOP代理协同问题

Spring三级缓存机制深度解析:如何解决循环依赖与AOP代理协同问题 1. 循环依赖一个看似简单却暗藏玄机的“死锁”在Java后端开发尤其是基于Spring框架的项目中循环依赖是一个老生常谈却又极易踩坑的问题。想象一下这个场景你正在开发一个电商系统有一个OrderService需要调用PaymentService来处理支付而PaymentService在支付成功后需要回调OrderService来更新订单状态。于是你自然而然地写出了这样的代码在OrderService中注入PaymentService在PaymentService中注入OrderService。项目启动你信心满满地点击运行然后控制台毫不留情地抛出了一个BeanCurrentlyInCreationException告诉你发生了循环依赖Spring容器无法完成Bean的创建。这个错误信息对新手来说可能有点懵但对于有经验的开发者它指向了一个经典的“先有鸡还是先有蛋”的困境。Spring容器在初始化时需要按照一定的顺序创建和装配所有的Bean。当两个或多个Bean相互引用时容器就陷入了僵局要创建A需要先有B但要创建B又需要先有A。这就像两个人隔着一条河河上只有一条独木舟两个人都需要对方先过河把船划过来结果就是谁也无法过河形成了死锁。为什么Spring不直接禁止循环依赖呢从设计原则上看循环依赖确实是一种代码的“坏味道”它增加了模块间的耦合度使得代码难以理解和维护。一个设计良好的系统应该遵循依赖倒置原则通过接口或抽象层来解耦。然而现实世界的业务逻辑往往是复杂且相互关联的完全避免循环依赖有时会迫使开发者写出更绕、更不直观的代码。因此Spring作为一个务实的框架并没有完全封死这条路而是提供了一套机制来“解决”它。注意这里的“解决”是带引号的更准确地说Spring是“支持”或“处理”了循环依赖其核心机制就是我们今天要深入剖析的“三级缓存”。2. 三级缓存机制Spring的“时空穿梭”魔法要理解Spring如何处理循环依赖我们必须深入到其Bean生命周期的核心——DefaultSingletonBeanRegistry这个类。在这里Spring维护了三个非常重要的Map也就是我们常说的“三级缓存”。它们就像是三个不同阶段的“接待室”用来存放处于不同创建状态的Bean。2.1 三级缓存的定义与角色第一级缓存singletonObjects。这是最终的“成品仓库”。当一个Bean完成了所有初始化步骤实例化、属性填充、初始化方法调用后就会被放入这个Map中。之后任何地方请求这个BeanSpring都会直接从这里面返回。它是ConcurrentHashMap保证了线程安全。第二级缓存earlySingletonObjects。你可以把它理解为“半成品展示厅”。这里存放的是已经实例化即调用了构造方法对象已经在堆内存中创建出来了但尚未完成属性填充和初始化的Bean。这些Bean是“早期引用”它们的属性可能还是空的或者填充的是原始类型的默认值。这个缓存的存在主要是为了解决循环依赖中需要引用一个尚未完全初始化好的Bean的场景。第三级缓存singletonFactories。这是最核心、也是最容易让人困惑的一级缓存。它存放的不是Bean对象本身而是ObjectFactory对象工厂。这个工厂的作用是当需要获取某个Bean的早期引用时它会调用getObject()方法。这个方法内部可能会执行一些非常重要的逻辑比如生成代理对象。对于普通的Bean它可能直接返回刚实例化出来的原始对象但对于被AOP切面代理的Bean它就会在这里通过后置处理器提前生成代理对象。这是解决“代理对象循环依赖”的关键。2.2 Bean创建流程与缓存互动让我们结合一个简单的循环依赖例子A依赖BB依赖A来走一遍Spring的创建流程看看三级缓存是如何介入的。开始创建ASpring容器启动开始创建单例Bean A。首先它会去singletonObjects一级缓存里找没有。于是它调用A的构造方法在堆内存中创建了一个A的原始对象。此时A对象是“裸”的里面的b属性是null。A进入第三级缓存在实例化之后Spring会立即将一个ObjectFactory放入singletonFactories三级缓存。这个工厂封装了创建A早期引用的逻辑。这是一个非常关键的操作它相当于为A这个“半成品”开了一个后门通道。填充A的属性Spring开始为A进行属性填充populateBean。它发现A依赖B于是需要去获取B的实例。开始创建B获取B的过程和A一样。先去一级缓存找没有然后实例化B并将B的ObjectFactory放入三级缓存。填充B的属性Spring开始为B填充属性发现B依赖A。于是它需要去获取A的实例。B获取A循环依赖的解决点B首先去一级缓存singletonObjects找A没有因为A还没完全创建好。然后去二级缓存earlySingletonObjects找A此时也没有。关键一步B去三级缓存singletonFactories找发现了A的ObjectFactory。它立刻调用这个工厂的getObject()方法。这个方法会做一件事判断A是否需要被代理比如是否有AOP切面。如果不需要就直接返回刚才创建的那个A的原始对象如果需要就通过AbstractAutoProxyCreator等后置处理器提前为A生成代理对象。无论返回的是原始对象还是代理对象这个对象都会被放入二级缓存earlySingletonObjects同时从三级缓存中移除对应的ObjectFactory。B成功拿到了A的早期引用可能是原始对象也可能是代理对象并将其注入到自己的属性中。至此B的属性填充完成。B完成初始化B继续执行后续的初始化方法如PostConstruct然后被放入一级缓存singletonObjects并从二级和三级缓存中清理掉。A继续填充属性此时B已经在一级缓存中了。Spring回到第3步顺利地从一级缓存拿到完整的B实例将其注入到A的b属性中。A完成初始化A执行自己的初始化方法然后也被放入一级缓存singletonObjects清理其他缓存。整个流程下来你会发现三级缓存的核心价值在于提供了一个“钩子”ObjectFactory。这个钩子允许Spring在Bean还未完全初始化时就能根据最终的需要是否要代理来提供一个正确的引用。如果没有三级缓存只有二级缓存那么对于需要代理的BeanSpring将无法在循环依赖发生时提供正确的代理对象因为代理对象通常是在初始化之后才生成的。注意这里有一个常见的误解认为三级缓存是为了解决“性能”问题。实际上它的核心目的是解决“代理对象”在循环依赖中的正确性问题。性能提升避免重复执行ObjectFactory逻辑是附带的好处。3. 为什么是三级而不是两级很多人在理解了流程后会产生一个疑问看起来二级缓存earlySingletonObjects已经可以存放早期引用了为什么还需要三级缓存singletonFactories这个工厂层直接把早期对象放进二级缓存不行吗答案是不行这会导致一个严重的问题——代理对象不一致。让我们设想一个只有两级缓存的场景假设只有singletonObjects和earlySingletonObjects并且Bean A需要被AOP代理。创建A实例化后我们直接将A的原始对象放入earlySingletonObjects。创建BB依赖A于是从earlySingletonObjects中拿到了A的原始对象并注入。B初始化完成放入一级缓存。A继续初始化。在初始化完成后AOP后置处理器会为A生成一个代理对象我们称之为A_Proxy。此时容器里就出现了两个“A”二级缓存里是A的原始对象一级缓存里是A的代理对象A_Proxy。而B里面注入的是那个原始对象。这就导致了严重的不一致B引用的A和最终容器里管理的A不是同一个对象后续如果通过B调用A的方法切面逻辑将不会生效因为调用的是原始对象而不是代理对象。这完全违背了AOP的预期。三级缓存通过ObjectFactory将“决定最终对象形态”的逻辑延迟到了第一次被索取早期引用时。当B通过三级缓存索取A时ObjectFactory.getObject()会即时判断并生成代理对象如果需要然后将这个最终形态的早期引用放入二级缓存。这样无论后续是B引用还是A自己完成初始化大家看到的、用到的都是同一个对象要么都是原始对象要么都是同一个代理对象保证了容器内Bean引用的唯一性和一致性。所以第三级缓存本质上是Spring AOP和循环依赖机制能够协同工作的基石。它确保了在存在循环依赖且需要代理的情况下所有依赖方拿到的是同一个、最终形态的Bean引用。4. 并非万能三级缓存的局限性虽然三级缓存机制非常巧妙但它并不是解决循环依赖的银弹它有着明确的适用范围和前提条件。理解这些限制能帮助我们在设计时主动避免问题。4.1 构造器注入导致的循环依赖无法解决这是三级缓存失效的最典型场景。回顾一下三级缓存的工作流程它的前提是Bean必须先完成实例化调用构造方法。实例化之后才能将ObjectFactory放入三级缓存为其他Bean提供早期引用。如果循环依赖是通过构造器注入发生的比如Component public class A { private B b; Autowired public A(B b) { // 构造器注入B this.b b; } } Component public class B { private A a; Autowired public B(A a) { // 构造器注入A this.a a; } }那么Spring在创建A时第一步就是调用A的构造方法。但构造方法需要B的实例作为参数于是Spring必须先去创建B。而创建B的第一步调用B的构造方法又需要A的实例作为参数。这就形成了一个死结在实例化阶段调用构造方法就卡住了根本没有机会走到“实例化后-放入三级缓存”那一步。因此Spring会直接抛出BeanCurrentlyInCreationException。解决方案将至少一方的注入方式改为属性注入Autowired在字段上或Setter方法注入。这样Spring就可以先调用无参或默认构造器完成实例化将对象工厂放入三级缓存从而打破僵局。4.2 多例Prototype作用域的Bean不支持循环依赖Spring的三级缓存只针对单例SingletonBean。对于原型PrototypeBeanSpring容器不会缓存它们每次请求都会创建一个新的实例。因此根本不存在一个共享的“早期引用”可供其他Bean使用。如果原型Bean之间发生循环依赖Spring同样无法处理。4.3 异步初始化与循环依赖的微妙冲突在Spring中你可以使用Lazy注解进行延迟初始化或者通过配置设置全局的延迟初始化。这本身是一种解决循环依赖的思路不真正注入一个完整的Bean而是注入一个代理当第一次调用该Bean的方法时才触发其初始化。但是如果结合不当可能会产生意想不到的效果。例如A依赖BB依赖A且A被标记了Lazy。Spring可能会成功启动因为Lazy打破了对立即初始化的需求。然而在后续运行时当真正触发A或B的初始化时可能会因为复杂的代理层级关系引发一些难以调试的问题。虽然这不是三级缓存直接导致的失败但属于循环依赖场景下的一个设计隐患。4.4 设计层面的警示Spring提供三级缓存机制是给我们一个“逃生舱”而不是鼓励我们随意制造循环依赖。从架构设计角度看循环依赖是高耦合的体现它会让代码变得脆弱单元测试难以进行模块复用性降低。一个更好的实践是应用分层严格遵循MVC、DDD等分层架构确保依赖方向是单向的如Controller - Service - Repository。依赖倒置引入接口让高层模块和低层模块都依赖于抽象。事件驱动使用Spring的ApplicationEvent机制。在上面订单和支付的例子里PaymentService支付成功后可以发布一个PaymentSuccessEvent事件由OrderService监听这个事件来更新状态从而解除两者间的直接依赖。方法提取如果两个Service确实需要紧密交互考虑将公共逻辑提取到一个第三方的工具类或新的Service中。记住能够被工具解决的问题不代表它就是一个好的设计。三级缓存让我们在不得已时能继续前进但主动优化代码结构消除循环依赖才是提升项目健康度的根本。5. 从源码视角验证流程理论需要结合实践而阅读源码是理解框架行为最直接的方式。我们不需要通读所有源码但可以抓住几个关键点来验证上述流程。以Spring Framework 5.x为例关键类org.springframework.beans.factory.support.DefaultSingletonBeanRegistry关键方法getSingleton(String beanName, boolean allowEarlyReference)这是从缓存中获取Bean的核心方法。你可以清晰地看到它依次查询一级、二级、三级缓存的逻辑。addSingletonFactory(String beanName, ObjectFactory? singletonFactory)在Bean实例化后这个方法被调用将ObjectFactory加入三级缓存。getSingleton(String beanName, ObjectFactory? singletonFactory)这是创建Bean的主流程方法它内部会调用上面的addSingletonFactory。调试技巧创建一个简单的Spring Boot项目构造一个属性注入的循环依赖如A和B。在DefaultSingletonBeanRegistry的getSingleton(String, boolean)方法入口处打上断点。启动项目观察断点触发时的调用栈和缓存内容。你会看到在创建A的过程中当需要B时会触发创建BB在需要A时会调用getSingleton(“a”, true)此时allowEarlyReference为true于是它从三级缓存中拿到了A的ObjectFactory并执行。进一步你可以创建一个需要AOP代理的Bean比如添加一个Transactional注解重复上述调试。这时你会发现从三级缓存的ObjectFactory中获取到的已经是一个代理对象如JdkDynamicAopProxy的实例。通过源码跟踪三级缓存从抽象概念变成了你可以亲眼所见的、在内存中流转的数据结构整个解决机制会变得无比清晰和确信。6. 面试深度剖析与实战思考“Spring如何解决循环依赖”是一个经典的面试题。仅仅回答“通过三级缓存”是远远不够的这只能算及格。一个出色的回答应该展现出层层递进的理解第一层现象与解决结果问题解释什么是循环依赖以及它会导致BeanCurrentlyInCreationException。结果Spring通过三级缓存机制支持了对单例Bean、属性/Setter注入场景下的循环依赖处理。第二层核心机制流程详细阐述清晰地说明三级缓存singletonObjects,earlySingletonObjects,singletonFactories各自的作用。流程描述以A依赖BB依赖A为例分步骤描述Bean创建过程中三级缓存是如何被查询和更新的。重点突出“实例化后立即加入三级缓存”以及“通过三级缓存工厂获取早期引用可能生成代理”这两个关键动作。第三层设计原理与深度为什么是三级解释两级缓存只有早期对象缓存在遇到AOP代理时会导致的“对象不一致”问题。强调三级缓存中的ObjectFactory是一个“智能工厂”它确保了在循环依赖被触发时能返回一个最终形态一致的对象原始对象或代理对象。局限性与边界明确指出该机制不适用于构造器注入、多例Bean等场景。并引申出这是Spring在“设计原则”和“开发便利”之间的一种权衡。第四层架构启示与最佳实践批判性思考指出尽管Spring提供了解决方案但循环依赖本身是代码高耦合的信号。分享在实际项目中如何通过事件驱动、接口分离、提取公共组件等方式来避免循环依赖追求更优雅的架构。在实战中当你真的遇到循环依赖问题时排查思路应该是确认错误类型是否是BeanCurrentlyInCreationException检查注入方式是否涉及构造器注入如果是考虑改为属性注入或Setter注入作为临时解决方案并规划重构。检查Bean作用域是否涉及多例Bean分析依赖链使用Spring启动时的日志设置debug级别或IDE的依赖分析工具找出具体的循环依赖链。评估设计这个循环依赖是否合理能否通过提取接口、引入事件、重新划分职责来彻底消除三级缓存是Spring框架精妙设计的一个缩影它用一套相对复杂的内部机制为开发者屏蔽了依赖管理中的一大难题。理解它不仅是为了应对面试更是为了在遇到相关问题时能快速定位根因并做出更优的架构决策。毕竟知道逃生舱在哪里很重要但更重要的是驾驶飞船不要轻易飞入需要逃生舱的险境。
返回列表