ARTICLE DETAIL

资讯详情

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

Spring循环依赖你还在靠

Spring循环依赖你还在靠 接手一个老项目时我看到了一段让我头皮发麻的代码ServiceA注入ServiceBServiceB注入ServiceCServiceC又注入ServiceA每个循环的边上都挂着Lazy注解。原作者离职了留下一个注释“不写Lazy启动报错加上就好了原因未知。”这就是典型的“用创可贴治骨折”。Spring循环依赖不是靠Lazy能糊弄过去的它暴露的是你代码结构里的深层问题。什么是循环依赖简单说A依赖BB依赖A形成一个环。Spring在创建Bean时先把A实例化发现要注入B去创建BB又发现要注入A此时A还没创建完于是陷入死循环。如果Spring不介入直接栈溢出。Spring默认只解决单例Bean的字段注入和setter注入循环依赖靠的是三级缓存。但构造器注入的循环依赖Spring直接放弃因为对象都创建不出来没法提前暴露。为什么说Lazy是创可贴Lazy的原理是延迟加载注入时先塞一个代理对象真正调用方法时才去容器里拿真实Bean。这样环就被“打断”了启动不报错了。但它有三个致命问题第一问题被隐藏了。代码结构本身的设计缺陷还在只是被推迟到运行时才暴露。第二可能引发BeanCurrentlyInCreationException。在某些代理场景下Lazy反而让问题更隐蔽排查更难。第三团队新人看不懂。一个环上挂三四个Lazy谁也不知道当初为什么加的改一处就崩。循环依赖的本质是职责划分不清回到那个老项目我花了半天梳理三个Service的调用关系发现真相是ServiceA和ServiceC互相调用的方法根本不属于它们各自的职责。ServiceA里有个校验逻辑本质上是ServiceC的领域知识ServiceC里有个通知逻辑应该由ServiceA通过事件发布。也就是说循环依赖不是技术问题是设计问题。两个类互相需要往往意味着有一个“第三者”没被抽出来。正确的解法从根上拆环解法一抽接口或抽服务。把共同依赖的部分下沉到第三个Bean里让A和C都依赖D环就断了。这是最干净的做法。解法二用事件解耦。A做完事后发布事件C监听事件。A不需要知道C的存在单向依赖没有环。解法三如果实在拆不动用ApplicationContext.getBean()延迟获取。但这应该是最后手段且必须写清楚注释。解法四构造器注入优先字段注入靠后。构造器注入能让你在启动时就发现循环依赖而不是等到运行时。Spring官方也推荐构造器注入。Spring三级缓存到底怎么工作的顺便说清楚Spring能解决字段注入的循环依赖靠的是三级缓存一级缓存存完全初始化好的Bean。二级缓存存提前暴露的原始Bean未填充属性。三级缓存存Bean工厂用于生成代理对象。A实例化后先把工厂放进三级缓存然后填充属性发现需要B。B实例化后填充属性发现需要A于是从三级缓存拿到A的工厂生成早期引用注入。B创建完成后A继续填充最终完成。整个过程只对单例、字段/setter注入有效。构造器注入和多例BeanSpring不解决。写在最后下次遇到循环依赖别急着加Lazy。先问三个问题这两个类为什么互相需要有没有第三个类该被抽出来能不能改成单向依赖加Lazy只需要一秒钟但拆环能让你的代码在半年后还被人看懂。Spring帮你兜底是情分不是本分。真正的好架构是连让Spring兜底的机会都不给它。
返回列表