news 2026/9/9 14:38:09

Spring循环依赖深度解析:三级缓存原理与源码实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring循环依赖深度解析:三级缓存原理与源码实战

循环依赖这个话题,在Spring面试里几乎是必问项,在真实项目里也经常踩坑。我见过不少同事,代码跑起来报了个BeanCurrentlyInCreationException,一脸懵地来问我“这啥意思”,然后我一看,好嘛,两个Service互相new来new去,典型的构造器循环依赖。

这篇文章我就把Spring解决循环依赖这件事彻底讲透。从最基础的概念,到三级缓存的设计原理,再到源码层面的执行流程,最后聊聊Spring Boot 2.6之后官方默认禁止循环依赖这件事,以及我们实际项目里到底该怎么处理。不管你是准备面试还是排查线上问题,这篇都能给你一个完整的参考。

先说清楚一个前提:Spring解决循环依赖有一个非常严格的前提条件,必须是单例Bean(Singleton),而且注入方式不能是构造器注入(有例外,后面细说)。脱离这两个大前提谈Spring解决循环依赖,都是耍流氓。

1. 循环依赖是什么:先分清Bean之间是怎么“绕圈”的

在深入源码之前,我们先把问题本身想清楚。

1.1 概念还原:两个或多个Bean相互引用

循环依赖,说人话就是:A对象创建时需要用到B对象,B对象创建时需要用到A对象,形成一个环。比如:

@Component public class A { private final B b; public A(B b) { this.b = b; } } @Component public class B { private final A a; public B(A a) { this.a = a; } }

上面这种就是典型的循环依赖。A要实例化,得先有B;B要实例化,得先有A。如果处理不好,这就是一个死锁,程序直接卡死或者报错。

更常见的还有三个甚至更多Bean形成的环:

@Component public class A { @Autowired private B b; } @Component public class B { @Autowired private C c; } @Component public class C { @Autowired private A a; }

A依赖B,B依赖C,C依赖A,绕了一圈又回来了。这种情况在业务代码里不算罕见,尤其是当Service层的拆分粒度不够清晰的时候。

1.2 注入方式差异:为什么Setter注入能解,构造器注入却不行

循环依赖能不能被解决,很大程度上取决于两个Bean是通过什么方式互相引用的。我们把情况拆开看:

构造器注入的循环依赖:A的构造器需要B,B的构造器需要A。创建A的时候发现得先创建B,创建B的时候发现得先创建A……二者都在等对方构造完成,谁都无法先创建出来。这就像两个人站在独木桥中间,谁也不肯先退一步,结果谁也别想过桥。Spring对这种情况无能为力,直接抛BeanCurrentlyInCreationException

Setter/字段注入的循环依赖:A的创建分为两步——先通过构造器把对象new出来(此时A还是一个“半成品”,B属性是null),再往A里填充B属性。只要A能先把“空壳”对象创建出来,让B那边有的可依赖,问题就解决了。B同样可以先new出空壳,再回头把A填进去。整个过程的核心是:对象实例化和对象属性初始化被拆成了两步,中间留出了一个“提前曝光”的空间。

注意:这里说的是“Spring框架本身无法解决构造器循环依赖”,不是“代码无法解决”。后面我会专门讲怎么通过@Lazy或重构来处理构造器循环依赖。

1.3 单例与原型:为什么Prototype的Bean救不回来

除了注入方式,Bean的作用域也是硬性限制。

  • 单例(Singleton):整个容器里就一个实例,每个Bean都有明确的“缓存槽位”,所以可以将“已实例化但未完全初始化”的早期对象暂存起来,供其他Bean先行引用。
  • 原型(Prototype):每次获取都是新对象,Spring没有地方保存它的“半成品”,更不可能等它初始化完再给下一个Bean用。原型Bean的循环依赖100%会报错。

简单记忆:循环依赖的解决方案本质上是“缓存复用”,而缓存复用的前提是“这个Bean最终只有一份”

2. 三级缓存机制:Spring解决循环依赖的底层设计

要说清楚Spring怎么解决循环依赖,三级缓存是绕不开的核心。但很多文章一上来就扔三个Map,搞得人一头雾水。我换个思路,先问你一个问题:如果你来设计这个机制,要解决“A依赖B、B依赖A”的问题,你会怎么做?

你大概率会想:行啊,我先创建A的空壳,把这个空壳放到某个地方暂存,然后去创建B,B里面填A的时候,从暂存区把那个空壳A拿来用,B就创建成功了,回头再把A的B属性填上不就完事了吗?

没错,Spring的思路跟你一模一样,但多了一个关键设计——它用了三层Map而不是一层。

2.1 三个Map各管什么:从“成品区”到“半成品区”

Spring里这三级缓存定义在DefaultSingletonBeanRegistry中,是三个成员变量:

// 第一级缓存:存放已经完全创建好的单例Bean private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 第二级缓存:存放早期暴露的单例Bean(Bean已经实例化,但属性未填充完) private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); // 第三级缓存:存放单例Bean的ObjectFactory(用于生成早期暴露的对象) private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

我习惯用大白话解释它们:

  • 一级缓存(成品区):Bean已经完整创建,可以直接使用了。
  • 二级缓存(半成品区):Bean刚被new出来,属性还没填充完。它存在这里的唯一意义,就是让其他Bean在循环依赖时能拿到它的引用。
  • 三级缓存(半成品的工厂区):存放的不是Bean本身,而是一个ObjectFactory函数式接口。这个工厂能产出Bean的早期引用(原始对象或代理对象)。

你可能已经看出来了:二级和三级缓存之间存在一个“升级”关系。当某个Bean从三级缓存里取出并放入二级缓存后,三级缓存里那个工厂就会被移除。换句话说,同一时刻,一个Bean只能出现在二级或三级缓存之一,但不会同时挂在两级缓存里

2.2 缓存查找顺序:为什么是逐级向下找

Spring在获取单例Bean的时候,会严格按照一级、二级、三级的顺序查找:

protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); // 一级缓存没有,且当前Bean正在创建中 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { singletonObject = this.earlySingletonObjects.get(beanName); // 二级缓存没有,且允许提前引用 if (singletonObject == null && allowEarlyReference) { synchronized (this.singletonObjects) { singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null) { ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }

注意几个细节:

第一,isSingletonCurrentlyInCreation(beanName)这个判断很关键。如果一个Bean压根没在创建中,那确实可能还没开始创建,而不是处于“半成品”状态,这时候不能从二三级缓存里拿。

第二,从三级缓存取出工厂并调用getObject()之后,结果会被放到二级缓存,同时移除三级缓存里的工厂。这么做的目的是防止同一个工厂方法被多次调用,导致同一个Bean被重复创建或者代理对象被重复生成。

第三,整个查询过程加了synchronized锁,是为了防止多线程环境下并发创建同一个Bean时出现状态不一致。

2.3 为什么必须是三级缓存:二级到底行不行

这是面试最爱追问的点,也是真正考验理解深度的地方。

很多人以为三级缓存是为了解决性能问题,多一层就多一次判断。其实三级缓存的核心目的只有一个:延迟代理对象的创建时机

假设我们的代码里有这样一个场景:A和B互相依赖,而且A需要被AOP代理(比如方法上有@Transactional注解)。

先看二级缓存方案会怎么走:

  1. A实例化完成,发现A需要代理。
  2. Spring把A的代理对象放入二级缓存。
  3. B创建时从二级缓存拿A的代理对象,注入成功。
  4. B创建完成,A继续完成属性填充、初始化。
  5. 最后A也被完整创建。

看起来能跑通,但问题出在第二步:在创建A的代理对象时,A的属性还没填充、BeanPostProcessor还没完整执行完。AOP代理通常是通过AnnotationAwareAspectJAutoProxyCreator这个BeanPostProcessorpostProcessAfterInitialization方法创建的,这个方法在Bean初始化完成之后才调用。如果在实例化阶段就提前创建代理对象,此时A内部的依赖都还是null,代理的逻辑可能基于一个不完整的状态。

用二级缓存也能解决,只要在实例化阶段就创建好代理对象,并且此后不再改变即可。但这会破坏Spring声明式AOP的一个重要特性:代理创建的时机应在Bean初始化完成之后

三级缓存的设计思路更巧妙:我们把代理创建的“决定权”往后推迟。

三级缓存里存的是一个ObjectFactory,它返回的不一定是原始Bean,也可以是代理Bean。这个工厂的getObject()方法内部会回调SmartInstantiationAwareBeanPostProcessorgetEarlyBeanReference方法,在这里判断是否需要创建代理。

关键逻辑来了:

  • 如果没有循环依赖,这个ObjectFactory可能从头到尾都不会被调用,Bean走的就是正常的生命周期:实例化 -> 属性填充 -> 初始化 -> 创建代理。代理对象只创建一次,时机正确。
  • 如果发生循环依赖,在“被其他Bean需要”的那个时间点,Spring才通过ObjectFactory获取早期引用。此时如果判断需要代理,就生成代理对象放入二级缓存;如果不需要代理,直接返回原始对象。

说白了,三级缓存让Spring具备了一个“按需提前代理”的能力,而这一切并不会影响非循环依赖场景下AOP的正常创建时机

用自己的话说:二级缓存要求Bean“创建后立刻决定是否代理”,而三级缓存允许Spring“等到有人需要时再决定是否代理”。聪明就聪明在这个“延迟决策”上。

3. 源码流程拆解:一个A依赖B、B依赖A的完整创建过程

理论说清楚了,我们来走一遍真实的源码流程。为了不让你被源码细节淹没,我用最典型的“A依赖B,B依赖A,且都是Setter注入”场景来演示。

3.1 第一次getSingleton:从一级缓存查起

假设容器启动时先创建A。

AbstractBeanFactory.doGetBean方法里会调用getSingleton(beanName),此时一级缓存、二级缓存、三级缓存全都为空,当前也没有“正在创建中”的记录,所以直接跳过缓存查询,进入创建逻辑。

public Object getSingleton(String beanName) { return getSingleton(beanName, true); } public Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); // ... return singletonObject; }

拿到null之后,Spring调用getSingleton(String beanName, ObjectFactory<?> singletonFactory)进入带工厂的创建方法:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { synchronized (this.singletonObjects) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { // 标记当前Bean正在创建中 beforeSingletonCreation(beanName); boolean newSingleton = false; try { // 核心:调用createBean singletonObject = singletonFactory.getObject(); newSingleton = true; } catch (BeanCreationException ex) { // ... } finally { afterSingletonCreation(beanName); } // 放入一级缓存 addSingleton(beanName, singletonObject); } return singletonObject; } }

这个synchronized锁很有讲究,后面排查并发问题的时候会用到,先记着。

3.2 doCreateBean:实例化后立刻“提前曝光”

createBean调用链会经过AbstractAutowireCapableBeanFactory.doCreateBean。这里发生了整个循环依赖解决过程中最关键的一步——提前曝光

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) { BeanWrapper instanceWrapper = null; // 1. 实例化Bean,调用构造器new一个对象出来 if (mbd.isSingleton()) { instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } if (instanceWrapper == null) { instanceWrapper = createBeanInstance(beanName, mbd, args); } Object bean = instanceWrapper.getWrappedInstance(); // ... // 2. 提前曝光:将ObjectFactory放入三级缓存 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } Object exposedObject = bean; // 3. 填充属性(这里会触发B的创建) populateBean(beanName, mbd, instanceWrapper); // 4. 执行初始化方法、BeanPostProcessor等 exposedObject = initializeBean(beanName, exposedObject, mbd); // ... return exposedObject; }

重点看第2步。addSingletonFactory方法把一个Lambda表达式放进了singletonFactories

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); } } }

这个Lambda内部是getEarlyBeanReference,它的作用我们前面提过:在真正需要早期引用的时候,判断是否要生成代理对象。

刚才创建出来的A对象,此时属性B还是null,但它已经被“半曝光”了。这里有个很反直觉的地方:Spring并不是等A完全创建好才放缓存,而是实例化完成后立刻就把自己标记为可获取

3.3 populateBean:填充属性时发现要找B

接着往下走,populateBean要对A做属性填充。A依赖B,所以要调用getSingleton("b")获取B。

B同样不在缓存里,所以B走和A一样的创建流程:

  1. 实例化B对象。
  2. 在B的属性填充阶段发现需要A。
  3. 调用getSingleton("a", true)获取A。

这次情况不一样了。当前A处于“正在创建中”的状态(前面beforeSingletonCreation标记过),一级缓存里没有A,所以我们进入二级缓存查询逻辑。

二级缓存earlySingletonObjects里没有A,继续到三级缓存singletonFactories里找。

找到了!A的三级缓存里存着一个ObjectFactory,调用它的getObject()方法:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { exposedObject = bp.getEarlyBeanReference(exposedObject, beanName); } } return exposedObject; }

如果没有AOP拦截,直接返回原始A对象。如果有AOP需求,则返回代理对象。

然后把这个结果放入earlySingletonObjects,同时从singletonFactories移除A的工厂。

B拿到了A的早期引用,成功完成B的属性填充、初始化、创建,并被放入一级缓存。

B创建完成后,A的populateBean也拿到了B的完整对象,填进A的b属性里。接着A完成初始化、放入一级缓存。

整个过程结束。你看,关键时间线其实是:A先实例化并提前曝光 -> B创建 -> B从三级缓存拿到A的早期引用 -> B完成 -> A拿到B完成注入。这就是三级缓存的完整流程。

3.4 提前曝光后的二次检查:为什么initializeBean之后还要校验

这里有个很隐蔽的细节,很多讲循环依赖的文章都不提。在doCreateBean的最后,Spring有一段二次检查逻辑:

if (earlySingletonExposure) { Object earlySingletonReference = getSingleton(beanName, false); if (earlySingletonReference != null) { if (exposedObject == bean) { exposedObject = earlySingletonReference; } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { // ... throw new BeanCurrentlyInCreationException(beanName); } } }

这么做的目的很简单:如果有人提前拿到了A的早期引用,而A又在后续流程中被动态代理了,那就可能出现两个不同的“A”

看最后那几行代码的逻辑:如果getSingleton(beanName, false)返回非null,说明A曾经被提前引用过(发生在循环依赖中)。如果当前exposedObjectbean还是同一个对象,说明A后来没有发生变化,直接用早期引用即可。但如果exposedObject != bean,说明Bean在初始化时被某个BeanPostProcessor替换成了新的对象(比如AOP生成代理),此时如果还存在其他Bean依赖了旧的A,就会出错,Spring会抛异常。

这个检查就是为了保证:所有依赖A的Bean拿到的必须是同一个对象

4. 为什么构造器注入无法解决:死锁场景拆解

前面已经提过构造器循环依赖无法被Spring解决,这里从执行机制上彻底拆透。你会发现这不只是Spring的局限,而是问题本身的无解。

4.1 构造器场景下三级缓存为何失效

三级缓存解决循环依赖的第一个前提就是Bean必须能先实例化出来

Setter注入能做到这一点,因为Spring可以先调用无参构造器(或者参数独立的构造器)创建空壳对象,然后把依赖关系放到populateBean阶段去解决。

构造器注入就不一样了。A的构造器形参是B,Spring在createBeanInstance阶段就必须解析构造器参数来调用构造器。此时bean还没创建出来,连“提前曝光”的机会都没有。我们回到代码逻辑看看:

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) { // 解析构造器:A(B b),发现参数B Constructor<?> constructorToUse = ...; // 需要调用getBean("b")来获取B Object[] argsToUse = ...; BeanWrapperImpl bw = new BeanWrapperImpl(); bw.setBeanInstance(ctor.newInstance(argsToUse)); return bw; }

Spring在调用A构造器之前需要解析参数B,于是先执行getBean("b")。B走创建流程,解析构造器时又需要参数A,于是再执行getBean("a")。可A此时还停留在“构造器参数解析”环节,它连实例都还没有,更不可能进入三级缓存,于是系统发现A正在创建中但三级缓存里没有它的引用,最终抛出BeanCurrentlyInCreationException

简单理解:实例化都没有完成的对象,不可能被提前引用。构造器让对象失去了“先出生后完善”的机会,所以三级缓存再巧妙也没用。

4.2 常见异常:BeanCurrentlyInCreationException的产生过程

实际项目里,构造器循环依赖最常见的报错长这样:

Error creating bean with name 'a' defined in file [A.class]: Requested bean is currently in creation: Is there an unresolvable circular reference?

这个异常信息明确指出:当前Bean正在创建中,但无法解析循环引用。说人话就是“你要找的Bean还没创建完,但它正卡在等待你的创建过程中,你俩互相等着,谁也动不了。”

有一种看似矛盾的场景也容易踩坑:虽然不是直接构造器循环依赖,但A通过构造器注入了B,B通过Setter注入了A。这种场景其实依然无法解决,因为Spring在创建A的构造器参数时就触发了B的创建,而B的Setter注入在填充属性时需要A的完整对象(A还没实例化完),一样会失败。判断的关键看触发依赖链的源头是不是构造器

5. Spring Boot 2.6之后的默认策略与我们该怎么写代码

在Spring Boot 2.6.0之前,循环依赖是默认允许的,项目里出现了循环依赖也只是默默跑着,大家也不太在意。但从2.6.0开始,官方默认禁止了循环依赖——只要启动时检测到,直接启动失败。

这个变化让不少人叫苦不迭,但其实这是个好事。我们来分析下背后的原因。

5.1 官方为什么默认禁用循环依赖

循环依赖从设计角度看,本身就是一种“代码坏味道”,它往往意味着两个模块耦合过重、职责边界模糊。很多循环依赖在实际运行时,依赖链中的某个Bean在每次请求中表现并不稳定,会引发各种隐藏Bug。

从性能层面看,循环依赖依赖的是“半成品Bean的提前引用”,而代理对象可能在生命周期早期就被创建,导致后续的某些AOP增强逻辑没有完整作用在最终对象上。这对事务、缓存等声明式功能有时会带来难以排查的问题。

另外,循环依赖的实现严重依赖三级缓存里singletonFactories这个Map,而它本身不是线程安全的(用的是普通HashMap),只是借住了singletonObjects这把锁来同步。一旦涉及并发创建单例Bean,这个锁竞争和状态迁移的复杂度会显著上升。官方显然是认为:与其冒这些风险,不如在框架层直接拉响警报。

5.2 实际的三种改造方案

如果你在Spring Boot 2.6+的项目中遇到循环依赖启动报错,有三种常规解法,按推荐程度排序:

方案一:重构代码,打破循环(最推荐)

两个Service互相调用通常不是好设计。可以尝试把公共的依赖提取到第三个类中,或者把A调用B的那部分逻辑下沉到一个独立的组件,让A和B都依赖这个新组件,而不是互相依赖。这是最彻底的解决方式,虽然涉及代码调整,但没有后患。

方案二:使用@Lazy延迟代理

如果不想大改代码,可以在依赖的注入点上加@Lazy

@Component public class A { private final B b; public A(@Lazy B b) { this.b = b; } } @Component public class B { private final A a; public B(@Lazy A a) { this.a = a; } }

@Lazy会让Spring为B和A分别生成一个延迟代理对象。创建A时,构造器里传入的并不是真正的B,而是一个B的代理;等真正调用B的方法时,代理才去解析并执行真正的B。这个方式能解决构造器循环依赖,但引入了一层代理,调用链上多了一次间接跳转,不算最优解。

方案三:用Setter或字段注入

如果循环依赖发生在Setter注入上,Spring Boot 2.6依然允许(前提是spring.main.allow-circular-references=true)。但要注意,这个开关只是兜底方案,不建议长期开启。沙盘演练时可以用,生产环境还是要想办法重构。

5.3 用了代理就高枕无忧了吗

很多开发者以为用了@Lazy就万事大吉,其实还有一个坑。

如果A和B互相依赖,而且A上有事务注解,Spring在创建A的代理对象时,可能会触发对B的真实解析。而B正在创建中且也需要代理,这个递归解析在某些复杂场景下会陷入死循环。我记得早年遇到过一个线上案例,A调用B的接口,B又调用A的另一个方法,两个方法都加了事务,在2.6版本报错后改成了@Lazy,结果启动没问题了,但调用时偶尔出现代理初始化顺序导致的空指针。后来我们干脆把公共逻辑抽成了C,彻底解耦,问题才根除。

6. 常见问题排查与经验总结

最后这部分,我整理了一些写代码和排查问题时实际会遇到的典型情况。

6.1 循环依赖问题排查清单

遇到BeanCurrentlyInCreationException或启动失败时,按这个顺序自查:

检查项判断标准解决方法
是否有构造器注入循环依赖类构造器参数里是否形成环增加@Lazy或重构
是否有原型Bean参与循环Bean是否配置了@Scope("prototype")改成单例,或重新设计
Spring Boot版本是否2.6+且未开启allow-circular-references开启临时开关,或重构
是否存在AOP+早期引用组合错误报错信息是否提到“Raw injection despite wrapping”重构,避免对半成品Bean做代理

6.2 我踩过的两个实际坑

第一个坑是多个线程并发获取同一个Bean时出现的诡异状态。项目里有一个定时任务和多线程任务同时触发,导致同一个未创建完成的Bean被多个线程并发访问,由于三级缓存加了synchronized锁,大部分情况都能挡住,但在某个高并发瞬间,偶尔会出现代理对象被创建两次的问题。排查了很久才发现是某个Bean的ObjectFactory被手动从外部调用触发的。后来我们规范了代码:不要在业务代码里直接操作DefaultSingletonBeanRegistry的缓存方法,这些内部方法只该被框架调用。

第二个坑是依赖了第三方库中的Bean,无法改源码。当时我们引用的一个内部SDK,它的两个组件类存在环状依赖,而且是构造器注入。升级Spring Boot 2.6后直接启动失败。我们没法改SDK源码,最终方案是在配置类里通过@Bean手动构造对象,绕开了组件扫描和自动装配。

6.3 面试与项目实战中的理解建议

如果你准备面试,关于循环依赖,多数面试官会问这几个角度:

  • 什么是循环依赖,Spring能解决哪种类型的循环依赖?
  • 三级缓存中每级的具体作用是什么?
  • 为什么不能用二级缓存解决?
  • 构造器注入循环依赖为什么无法解决?

深挖“为什么不能用二级缓存”时,重点讲清楚“代理创建时机”这个点,基本就能体现你的真实理解深度。

如果你是在项目实战中用到,记住一句我总结的话就够了:循环依赖不是框架的bug,也不是必须要支持的feature,它就是代码设计层面告诉你“该重构了”的信号。能用重构解决的,优先重构;确实需要临时快速处理的,用@Lazy过渡;都不行的,再考虑开启allow-circular-references

从源码细节回到日常开发,我个人最大的体会是:很多人把三级缓存背得滚瓜烂熟,但遇到实际报错还是不知道从哪里排查。原因就在于只记住了“三个Map”,没理解“实例化和初始化的分离是这一切能成立的前提”。搞懂了这个前提,再回头看构造器失败、原型失败、代理时机这些衍生问题,思路就通了。Spring这套设计虽然精巧,但它解决的终究是代码结构不理想时的兜底问题,日常写代码还是应该尽量避免制造循环依赖,这句话值得贴在工位上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 14:38:03

bRPC深度剖析:C++高性能RPC框架实战路径

bRPC深度剖析&#xff1a;C高性能RPC框架实战路径 【免费下载链接】brpc brpc is an Industrial-grade RPC framework using C Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. &qu…

作者头像 李华
网站建设 2026/9/9 14:37:49

手写Unity BlendTree:核心算法与PlayableGraph实现

1. 手写BlendTree之前&#xff1a;先搞懂我们到底在解决什么问题1.1 为什么放着现成的Animator Controller不用&#xff0c;非要写代码Unity的Animator Controller里本身就自带BlendTree节点&#xff0c;右键Create State就能加&#xff0c;拖几个Clip进去调调threshold就能跑。…

作者头像 李华
网站建设 2026/9/9 14:34:39

2026AI 学术工具哪家值得信赖,沁言学术合规要点

AI学术工具正在深度融入科研工作流——从文献检索到框架搭建&#xff0c;从逻辑梳理到格式校对&#xff0c;效率提升有目共睹。但硬币的另一面同样值得警惕&#xff1a;学术造假隐患、AI幻觉、数据泄露等问题频频出现&#xff0c;让不少科研人在"用与不用"之间反复犹…

作者头像 李华
网站建设 2026/9/9 14:34:23

企业级大模型聚合平台深度解析:从模型路由到选型落地

如果你最近在帮公司评估AI能力&#xff0c;Claude-Fable5这个名字大概率已经被同事抛到你面前好几次了。2026年聊大模型&#xff0c;大家早就不满足于“能跑通”&#xff0c;而是关心怎么把多个模型统一管起来、把成本压下去、把安全边界守住。Claude-Fable5在圈子里经常被当成…

作者头像 李华
网站建设 2026/9/9 14:34:12

Nginx安装全指南:源码编译、包管理器、Docker与离线部署一次讲透

做过几年服务器运维和Java后端的人&#xff0c;对Nginx应该都不陌生。前一阵同事在给测试环境搭新服务&#xff0c;照着网上教程吭哧吭哧装Nginx&#xff0c;结果configure那一步就报缺PCRE库&#xff0c;装完PCRE又发现没带SSL模块&#xff0c;反反复整了半天&#xff0c;最后…

作者头像 李华