获课:shanxueit.com/6847/
Spring循环依赖解决原理详解:复刻三级缓存核心逻辑的个人理解
Spring的循环依赖,可能是每一个Java开发者面试时的必考题,也是日常开发里最容易让人一头雾水的问题。我刚接触Spring的时候,一直被一个问题困扰:A依赖B,B依赖A,这两个Bean明明互相需要对方,Spring到底用了什么魔法把它们各自创建出来的?直到后来我自己动手实现了一个极简版的IOC容器,才真正理解了三级缓存这套设计的高明之处。这篇文章不讲源码,只聊聊我个人的理解过程。
循环依赖到底难在哪里
先想清楚一个最朴素的问题:在纯Java代码里,A的构造函数里new B,B的构造函数里new A,这段代码根本跑不起来,直接报栈溢出。因为创建A需要B先存在,创建B需要A先存在,谁也没法先于对方出生。
但Spring的Bean不全是构造器注入的,还有Setter注入和字段注入。如果是Setter注入,逻辑上就有一个突破口:我可以先把A的实例创建出来(但先不注入B),然后创建B的实例(注入一个半成品的A),最后回过头来把B注入到A里。先有对象实例,再组装依赖关系,这是解决循环依赖的核心思路。
三级缓存到底在解决什么问题
很多人直接看Spring的三级缓存源码会觉得一头雾水,什么singletonObjects、earlySingletonObjects、singletonFactories,三个Map绕来绕去。但如果站在设计者的角度去想,这个问题就清晰了。
Spring需要同时解决三个问题:第一,Bean要能被提前暴露给依赖它的其他Bean;第二,这个提前暴露的对象必须是个完整的实例(能调用方法),但依赖还没注入;第三,如果这个Bean最终被AOP代理了,那依赖它的Bean拿到的应该是代理对象,而不是原始对象。
一级缓存存放完全初始化好的成品Bean,二级缓存存放提前暴露的半成品Bean(已经实例化但还没注入依赖),三级缓存存放生成提前暴露对象的工厂。这三个层次解决的是同一个Bean在生命周期的不同阶段,对外提供不同形态的"自己"。
我自己的理解是:如果没有AOP,其实两级缓存就够了。三级缓存里的工厂不是为了创建Bean实例,而是为了处理AOP代理的延迟生成。因为代理对象在普通实例化之前是不存在的,只有等所有Bean创建完、AOP切面要织入的时候才知道这个Bean需不需要被代理。所以Spring放了一个工厂在三级缓存里,等真正需要获取这个Bean的时候,再由工厂决定是返回原始对象还是生成代理对象。
我动手复刻后的几个认知刷新
为了真正理解这套机制,我照着Spring的思路写了一个玩具版IOC容器,只保留了核心逻辑。动手写代码的过程让我对几个点有了新的认识。
第一个认知是:构造器注入的循环依赖是无解的。因为构造器注入要求在实例化阶段就完成依赖注入,没有"先实例化再注入"这个时间窗口,三级缓存再强也救不了。所以Spring官方建议对循环依赖的场景优先用Setter注入,这不是没有道理的。
第二个认知是:三级缓存的Key不是Bean的实例,是Bean的Name。这个细节我一开始想错了,以为缓存里存的是Bean对象。实际上存的是"将来要生成某个Bean的那个工厂"。正是因为这个设计,Spring才能在不知道这个Bean最终长什么样(原始对象还是代理对象)的情况下,先把位置占住。
第三个认知是:循环依赖解决的代价是性能开销。三级缓存的查找逻辑虽然看起来只是几个Map的get操作,但在Bean数量很大的情况下,这个开销会被放大。而且提前暴露对象会让Bean的生命周期变得不那么纯粹,调试的时候更难追踪。所以有些团队会把循环依赖视为代码坏味道,遇到了就主动重构,而不是一味依赖Spring的兜底能力。
一点个人看法
循环依赖解决机制是Spring非常亮眼的设计,但我始终觉得:能不用尽量不用。三级缓存这套东西更像是框架的一把安全网,保证你在不小心写出循环依赖的时候应用还能启动,而不是鼓励你故意写成这样。每一次用Setter注入来解决循环依赖,都值得停下来想一想:A和B是不是真的必须互相依赖?能不能通过引入一个中间层把双向依赖变成单向依赖?
技术上的巧妙设计,不应该成为放任代码腐化的借口。理解三级缓存是为了知道Spring在背后默默帮我们扛了什么,而不是为了在代码里更多地依赖它。真正好的设计,是让循环依赖根本不会出现在你的代码里——如果做不到,至少要知道Spring是怎么帮你兜底的。
本站不存储任何实质资源,该帖为网盘用户发布的网盘链接介绍帖,本文内所有链接指向的云盘网盘资源,其版权归版权方所有!其实际管理权为帖子发布者所有,本站无法操作相关资源。如您认为本站任何介绍帖侵犯了您的合法版权,请发送邮件
[email protected] 进行投诉,我们将在确认本文链接指向的资源存在侵权后,立即删除相关介绍帖子!
暂无评论