当前位置: 首页 > news >正文

spring详解-循环依赖的解决

Spring循环依赖

重点提示: 本文都快写完了,发现“丈夫” 的英文是husband… 在“②有AOP循环依赖” 改过来了,前面用到的位置太多了就没改。我是说怎么idea的hansband英文下面怎么有波浪线。各位能够理解意思就行,英文拼写不要过于在意.

1.案例引入

在这篇文章中,"②容器刷新"这一小节,留下了如下这样一个疑问。【https://blog.csdn.net/okok__TXF/article/details/147009731】

// DefaultSingletonBeanRegistry.java
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lockObject singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {// Consistent creation of early reference within full singleton locksingletonObject = 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;
}
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/okok__TXF/article/details/147009731

getSingleton(String beanName, boolean allowEarlyReference) 如果allowEarlyReference是true的话,就用三级缓存来解决循环依赖。下面以Spring5.3.31再次来追踪一下源码。

本文章示例代码见该仓库:【spring】中的“spring”模块。

仓库地址:https://gitee.com/quercus-sp204/sourcecode-and-demos

2.循环依赖分析

①无AOP的循环依赖

下面举一个例子,一个丈夫和一个妻子的循环依赖。

// 这是Main测试类
@Configuration
@ComponentScan("com.feng.myspring")
public class Main {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);Hansband h = context.getBean(Hansband.class);h.say();}
}//1.丈夫
public class Hansband {@Autowiredprivate Wife wife;public void say() {System.out.println("我是丈夫");wife.eat();}public Hansband( ) {}public void eat() {System.out.println("丈夫吃饭");}
}
//2.妻子
public class Wife {@Autowiredprivate Hansband hansband;public void say(){System.out.println("我是妻子");hansband.eat();}public Wife() {}public void eat() {System.out.println("妻子吃饭");}
}

然后再配置类里面创建了一个丈夫和妻子对象

@Configuration
public class Config01 {@Beanpublic Hansband hansband(){return new Hansband();}@Beanpublic Wife wife(){return new Wife();}
}

然后上面的项目创建好了之后,现在就开始分析如下方法

// DefaultSingletonBeanRegistry.java
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lockObject singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {// Consistent creation of early reference within full singleton locksingletonObject = 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;
}

首先理解一下这其中的变量都是啥。

  • singletonObjects:单例对象的缓存,bean名称对应一个bean实例。主要存放的是已经完成实例化、属性填充和初始化所有步骤的单例Bean实例,这样的Bean能够直接提供给用户使用,称之为终态Bean或叫成熟Bean。【一级缓存】
  • earlySingletonObjects:早期单例对象的缓存,bean名称对应一个bean实例。主要存放的已经完成实例化,但属性还没自动赋值的Bean,这些Bean还不能提供用户使用,只是用于提前暴露的Bean实例,把这样的Bean称之为临时Bean或早期的Bean(半成品Bean) 【二级缓存】
  • singletonFactories:单例工厂的缓存,bean名称对应一个对象工厂。存放的是ObjectFactory的匿名内部类实例,调用ObjectFactory.getObject()最终会调用getEarlyBeanReference方法,该方法可以获取提前暴露的单例bean引用。【三级缓存】
    在这里插入图片描述

上图是getSingleton(String beanName, boolean allowEarlyReference)方法的大致流程。然后就没了?肯定不是的。

回顾容器刷新

现在回顾一下容器刷新阶段,里面会调用这样的方法

// AbstractApplicationContext.java
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {...// Instantiate all remaining (non-lazy-init) singletons.beanFactory.preInstantiateSingletons();
}// DefaultListableBeanFactory.java
@Override
public void preInstantiateSingletons() throws BeansException {...// Trigger initialization of all non-lazy singleton beans...for (String beanName : beanNames) {RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {...getBean(beanName); // 会调用这个...}}...
}// AbstractBeanFactory.java
@Override
public Object getBean(String name) throws BeansException {return doGetBean(name, null, null, false);
}
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)throws BeansException {.......... // ===== 【重要】   
}

上面最后是到了AbstractBeanFactory::doGetBean(xxx)方法了。下面就来着重分析一下这个doGetBean. 这里只挑重点,并不是一行一行地来。

// 一、AbstractBeanFactory.java
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)throws BeansException {...// 1.检查单例缓存Object sharedInstance = getSingleton(beanName);if (sharedInstance != null && args == null) {....beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);}else {//2.原型作用域(prototype) 的 Bean 不支持循环依赖,//因为每次获取都会创建新实例,Spring 无法通过缓存解决循环依赖。if (isPrototypeCurrentlyInCreation(beanName)) {throw new BeanCurrentlyInCreationException(beanName);}....try {.....// 3.Create bean instance.if (mbd.isSingleton()) {// 4.调用了重载的getSingleton(xx,xx)方法===// 第二个参数是lambda表达式sharedInstance = getSingleton(beanName, () -> {try {return createBean(beanName, mbd, args);}catch (BeansException ex) {....}});beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);}...}return adaptBeanInstance(name, beanInstance, requiredType);
}// 二、DefaultSingletonBeanRegistry.java ----- 重载的getSingleton
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {....synchronized (this.singletonObjects) {Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {.....beforeSingletonCreation(beanName);boolean newSingleton = false;boolean recordSuppressedExceptions = (this.suppressedExceptions == null);if (recordSuppressedExceptions) {this.suppressedExceptions = new LinkedHashSet<>();}try {/* getObject()实际执行的是这个。上面的lambda() -> {try {return createBean(beanName, mbd, args);}catch (BeansException ex) {....}}*/// 实际调用的createBean(beanName, mbd, args);---见下面的三singletonObject = singletonFactory.getObject();newSingleton = true;}catch (IllegalStateException ex) {.....}.....finally {if (recordSuppressedExceptions) {this.suppressedExceptions = null;}afterSingletonCreation(beanName);}if (newSingleton) {addSingleton(beanName, singletonObject);}}return singletonObject;}
}// 三、AbstractAutowireCapableBeanFactory.java
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)throws BeanCreationException {...try {// 创建实例beanObject beanInstance = doCreateBean(beanName, mbdToUse, args);...return beanInstance;}catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) {...}...
}
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)throws BeanCreationException {// 1,得到bean实例,先从缓存中找,找不到就构造一个BeanWrapper instanceWrapper = null;//调用 createBeanInstance 方法根据 Bean 定义和构造函数参数创建一个新的 BeanWrapper 实例。//从 BeanWrapper 中获取实际的 Bean 实例和 Bean 的类型,并将类型信息记录到 Bean 定义中。instanceWrapper = createBeanInstance(beanName, mbd, args);....// 2.low post-processors to modify the merged bean definition.// 应用合并后的 Bean 定义后置处理器synchronized (mbd.postProcessingLock) {...applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);...}//3.提前暴露单例 Bean 以处理循环引用boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {....// 这里是不是就放到了三级缓存中去了,注意一下参数哦。。getEarlyBeanReference// 将一个 ObjectFactory 放入单例工厂缓存中,该工厂会调用 getEarlyBeanReference 方法// 该方法会返回一个早期的 Bean 引用,以便在循环依赖时可以提前获取到 Bean 的引用。addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}Object exposedObject = bean;try {// 4.填充bean属性populateBean(beanName, mbd, instanceWrapper);// 5.初始化bean【本文开头的上篇文章】exposedObject = initializeBean(beanName, exposedObject, mbd);}catch (Throwable ex) {......}if (earlySingletonExposure) {.....}...return exposedObject;
}

doCreateBean 方法是 Spring 框架中用于实际创建 Bean 实例的核心方法。它在 AbstractAutowireCapableBeanFactory 类中实现。该方法承担了创建 Bean 实例、属性填充、初始化以及处理循环依赖等重要任务。从最后一个方法我们可以大致总结三个关于spring创建bean的核心步骤,从上到下按顺序。

  • 实例化Bean : 可以理解为new一个空的bean对象 【createBeanInstance
  • 填充Bean属性 : 对bean的依赖属性进行填充,对@Value @Autowired @Resource注解标注的属性注入对象引用。【populateBean
  • 调用Bean初始化方法 : @PostConstruct、init-metohd、等等【initializeBean

这一节我们从最开始的getBean(beanName);,一直往下,发现getBean好像就是创建对象嚯。 【这句话后面会用到的。。。】

示例分析

那么案例中,我们是字段注入的,肯定就是发生在“填充Bean属性” 这个阶段才会产生的循环依赖问题!!接下来就以上面的“丈夫、妻子”为例子。

在这里插入图片描述

上图是正在创建Hansband的bean对象、到了属性填充这个步骤了。进入这个populateBean方法里面,

在这里插入图片描述

上图是populateBean里面,调用这个AutowiredAnnotationBeanPostProcessor :: postProcessProperties(xx)方法。

在这里插入图片描述

上图中,在这个方法里面,调用 findAutowiringMetadata 方法,根据 Bean 的名称、类型以及属性值集合来查找自动注入的元数据。自动注入元数据包含了 Bean 中需要自动注入的字段、方法等信息,这些信息是通过 @Autowired@Resource 等注解标记的。可以发现绿色箭头指向的是wife了。随后,调用metadata.inject(bean, beanName, pvs);来执行执行依赖注入操作。

在这里插入图片描述

上图是metadata.inject里面的内容,对目标对象执行依赖注入操作。for循环遍历注入元素并执行注入操作。 element.inject(target, beanName, pvs);

在这里插入图片描述

上图中,最后如果value不是null,field.set(bean, value):将解析好的依赖值注入到目标 Bean 的字段中。所以重点应该是上面的**resolveFieldValue 核心逻辑**

在这里插入图片描述

上图是解析字段的核心逻辑。

首先, 创建依赖描述符(DependencyDescriptor),可以看到将field传了进去,【field很明显就是wife嘛,上图中也可以看出来】。

然后,准备依赖解析环境…这个就不说了。其次,调用了这一行代码 beanFactory.resolveDependency(xxx);,这个是Spring 依赖解析的核心方法,根据 DependencyDescriptor 查找匹配的依赖值【可以很明显感觉到,这个又是核心逻辑了】。 找到依赖值后,会缓存起来。

最后会返回这个value嘛。

在这里插入图片描述

上图是beanFactory.resolveDependency,先会检查是否需要延迟解析代理(处理 @Lazy 等场景),若无需延迟解析,返回 null,进入下一步。然后,调用核心解析方法 doResolveDependency

// ContextAnnotationAutowireCandidateResolver.java
// 检查是否需要延迟解析
public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null);
}

接着进入doResolveDependency方法里面看看。

在这里插入图片描述

// DefaultListableBeanFactory.java
@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {...// 一阵噼里啪啦if (instanceCandidate instanceof Class) {instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);}
}// DependencyDescriptor.java
public Object resolveCandidate(String beanName, Class<?> requiredType, BeanFactory beanFactory)throws BeansException {return beanFactory.getBean(beanName); //
}

看到这里,明白了没,一切都回来了!!! 前面不是说过吗getBean好像就是创建对象嚯。我们在创建Hansband对象的时候,这个时候并没有创建Wife对象啊,现在兜兜转转又回来了。doGetBean() 现在里面是wife了。!!!!【递归进去,Hansband对象的创建还是卡在populateBean方法的】

在这里插入图片描述

然后Wife又会走一遍上面的流程。我们在最上面创建好Hansband之后,在填充Hansband属性之前,有这样一段代码。

//3.提前暴露单例 Bean 以处理循环引用
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {....// 这里是不是就放到了三级缓存中去了,注意一下参数哦。。getEarlyBeanReference// 将一个 ObjectFactory 放入单例工厂缓存中,该工厂会调用 getEarlyBeanReference 方法// 该方法会返回一个早期的 Bean 引用,以便在循环依赖时可以提前获取到 Bean 的引用。// 在后续再缓存中查找Bean时会触发匿名内部类getEarlyBeanReference()方法回调addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}//4. 填充属性
..... 
populateBean(beanName, mbd, instanceWrapper);

创建Wife的时候同理啊,又会调用一遍addSingletonFactory,此时是把wife放到singletonFactories里面了。此时singletonFactories里面就会有两个键值对。

hansband -- getEarlyBeanReference("hansband", mbd, bean)
wife -- getEarlyBeanReference("wife", mbd, bean)

然后就轮到执行Wife的属性填充了,发现需要Hansband的类型的bean,顺着上面梳理的流程,最后又会回到下面这里,只不过此时的参数:descriptor就是hansband,beanName是Wife了。

// DefaultListableBeanFactory.java
@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {...// 一阵噼里啪啦if (instanceCandidate instanceof Class) {instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);// beanFactory.getBean("hansband");  递归进去}
}

在Wife的属性填充的时候,递归进去拿Hansband对象,会有如下显示【这个时候,三级缓存里面就可以看到有两个键值对了,在这里将Hansband的早期对象拿到,并把它从三级缓存移动到二级缓存中去】

在这里插入图片描述

这样在Wife属性填充的时候,实现了提前将Hansband曝光给Wife完成属性依赖注入。紧接着,Wife就可以继续完成后面的初始化逻辑,产生一个成熟的Wife。

创建好之后,会addSingleton("wife", xxxx)

protected void addSingleton(String beanName, Object singletonObject) {synchronized (this.singletonObjects) {this.singletonObjects.put(beanName, singletonObject); // 放入一级缓存this.singletonFactories.remove(beanName); // 移除二三级缓存this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}
}

请看下面的流程图解释。

在这里插入图片描述

上图流程中,创建wife的时候,提前注入了一个hansband还没有初始化好的对象【注入了一个早期bean】,这样不会有什么问题吗?

wife创建完全之后,会返回到hansband的“bean初始化阶段”,然后hansband就会初始化ok并放入单例池。由于wife中的早期bean和 创建hansband中的bean是同一个引用,故没有啥问题的。

嘶,看了一下,要三级缓存有这个必要吗?图中二级缓存干啥的,getEarlyBeanReference经过调试,发现就是返回了一个bean。看来还是疑点重重,请看下一节。

②有AOP的循环依赖

在上面普通bean的循环依赖场景下,可以看出三级缓存貌似并没有什么卵用。【实际上确实是的,在普通的循环依赖的情况下,三级缓存没有任何作用。

】经过反复参考求证,发现三级缓存是和spring 的 AOP挂钩的!

AOP CSDN地址:https://blog.csdn.net/okok__TXF/article/details/147397816

AOP 博客园地址:https://www.cnblogs.com/jackjavacpp/p/18838920

看一下上一小节的getEarlyBeanReference(beanName, mbd, bean) 到底做了什么

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代理之前,这个方法中的SmartInstantiationAwareBeanPostProcessor如下所示【AutowiredAnnotationBeanPostProcessor::getEarlyBeanReference()方法就是单纯的返回bean】

在这里插入图片描述

打开AOP之后,变成了AnnotationAwareAspectJAutoProxyCreator~ 那么它的getEarlyBeanReference()方法有变化吗?

在这里插入图片描述

// AbstractAutoProxyCreator.java
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {Object cacheKey = getCacheKey(bean.getClass(), beanName);/*========== earlyProxyReferences【重要】跟踪哪些 Bean 的代理对象已在 提前暴露阶段 生成,他的主要作用大概如下1. 防止重复代理:避免在 Bean 初始化阶段重复创建代理对象,如果有循环依赖,那么该代理对象在属性填充阶段被创建过了2. 保证代理对象一致性:确保循环依赖注入的代理对象与最终暴露的代理对象是同一实例*/this.earlyProxyReferences.put(cacheKey, bean); // ======return wrapIfNecessary(bean, beanName, cacheKey); // 下一层
}
// 到这里来了
// 决定是否为给定的 Bean 创建代理对象
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {....// Create proxy if we have advice.// getAdvicesAndAdvisorsForBean获取适用于该 Bean 的通知和增强器// 该方法会根据 Bean 的类型和名称,从 Spring 容器中查找所有匹配的通知和增强器,并返回一个数组Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);// DO_NOT_PROXY 就是 nullif (specificInterceptors != DO_NOT_PROXY) { // 不为nullthis.advisedBeans.put(cacheKey, Boolean.TRUE);//调用 createProxy 方法创建代理对象Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;}this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;
}

可以大致了解到这个是返回了一个代理对象,可见开启AOP之前和开启AOP之后,果然有不一样。现在我们将案例变成如下有AOP的循环依赖。

// 接口
public interface AopMan {void aopManSay();
}
@Component("husband")
public class AopHusband implements AopMan{@Autowiredprivate AopWoman wife;@Overridepublic void aopManSay() {System.out.println("【AOP】Husband say 哦吼");}
}// 接口
public interface AopWoman { void aopWomanSay();
}
@Component("wife")
public class AopWife implements AopWoman{@Autowiredprivate AopMan husband;@Overridepublic void aopWomanSay() {System.out.println("【Aop】Wife say 哈哈");husband.aopManSay();}
}// 创建切面
@Component
@Aspect
public class ManAdvice {private static final String manExpression = "execution(* com.feng.myspring.aopobj.*.aopManSay*(..))";//环绕通知@Around(manExpression)public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("##########【环绕通知中的前置通知】##########");Object returnVale = joinPoint.proceed();System.out.println("##########【环绕通知中的后置通知】##########");return returnVale;}
}// 主启动测试类
@Configuration
@ComponentScan("com.feng.myspring")
@EnableAspectJAutoProxy(proxyTargetClass = false)
public class Main {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);// ②有Aop的循环依赖AopMan husband = context.getBean("husband", AopMan.class);husband.aopManSay();System.out.println(husband.getClass()); // 打印一下类型AopWoman wife = context.getBean("wife", AopWoman.class);System.out.println(wife.getClass()); // 打印一下类型}
}

上述例子中,wife的aopWomanSay()方法里面会调用husband的aopManSay方法,但是此时我是对husband的aopManSay配置了环绕通知的,那么请各位想一下此时wife里面注入的husband是原来的对象吗?肯定不是的,是代理对象,注入的是husband的代理对象,可以运行一下Main测试类看看结果【结果如下】

AopWife say 哈哈
##########【环绕通知中的前置通知】##########
【AOP】Husband say 哦吼
##########【环绕通知中的后置通知】##########
class com.sun.proxy.$Proxy18 // husband是一个代理对象
class com.feng.myspring.aopobj.AopWife// wife仍然是原来

我们都知道springbean的创建过程,是 1.缓存查询;2.创建对象;3.属性填充、注入依赖;4.执行初始化操作;5.这个bean创建好了。大致这样五个过程,在没有循环依赖的前提下,开启aop会生成bean的代理对象,这个生成代理对象的时机是在 “4.执行初始化操作” 这一步的,但是本文讨论的都是在第三步哦~~【AOP见下面文章】。所以,三级缓存的存在就是为了提前用对象工厂获取代理对象,并赋值给wife的husband属性【代理对象】依赖注入。

AOP CSDN地址:https://blog.csdn.net/okok__TXF/article/details/147397816

AOP 博客园地址:https://www.cnblogs.com/jackjavacpp/p/18838920

回到本小节开始的时候,开启aop之后,AnnotationAwareAspectJAutoProxyCreator通过getEarlyBeanReference里面的wrapIfNecessary拿到的代理实例,我们对husband进行了AOP代理的话,那么此时getEarlyBeanReference将返回一个代理后的对象,而不是实例化阶段创建的对象,这样就意味着wife中注入的husband将是一个代理对象而不是husband的实例化阶段创建后的对象。

Spring是在何处将husband的代理对象放进去的呢?

在这里插入图片描述

在完成husband初始化后,Spring又调用了一次getSingleton方法,允许早期引用为false。在前面分析的时候在为wife中注入husband时已经将三级缓存中的工厂取出,并从工厂中获取到了一个husband代理对象放入到了二级缓存中,并且从三级缓存中移除掉,所以这里的这个getSingleton方法做的时间就是从二级缓存中获取到这个代理后的husband对象。

3.疑问

1.为什么三级缓存要弄一个对象工厂添加进去,我直接往三级缓存放入对象的代理对象不行吗?

这个工厂的目的在于延迟对实例化阶段生成的对象的代理,只有真正发生循环依赖的时候,才去提前生成代理对象,否则只会创建一个工厂并将其放入到三级缓存中,但是不会去通过这个工厂去真正创建对象。

读者在此处思考一下,如果看过AOP,那么肯定就会知道,代理对象的生成是在bean的初始化操作中的后置处理器的postProcessAfterInitialization这一步的,而我们循环依赖发生在属性填充这一步。发生循环依赖的时候,需要注入代理对象,但是还没到代理对象生成那一步

try {// 4.填充bean属性populateBean(beanName, mbd, instanceWrapper);// 5.初始化bean【本文开头的上篇文章】exposedObject = initializeBean(beanName, exposedObject, mbd);
} .....

在 Spring 框架中,三级缓存(singletonFactories)存储的是 ObjectFactory,而非直接存储代理对象,这一设计是为了解决循环依赖中代理对象生成的 时机问题

2.用二级缓存不行吗?

循环依赖的简单场景(无代理) : 是没问题的

假设有两个 Bean:AB,它们互相依赖。Spring 的解决流程如下:

  1. 创建 A:实例化 A(调用构造方法),此时 A 还未完成属性填充和初始化。
  2. 暴露早期对象:将 A 的原始对象包装成 ObjectFactory,存入 三级缓存singletonFactories)。
  3. 填充 A 的属性:发现 A 依赖 B,开始创建 B。
  4. 创建 B:实例化 B,填充 B 的属性时发现依赖 A。
  5. 从三级缓存获取 A:通过 ObjectFactory.getObject() 获取 A 的早期引用(原始对象),注入到 B 中。
  6. 完成 B 的初始化:B 初始化完成后,存入一级缓存(singletonObjects)。
  7. 完成 A 的初始化:将 A 的最终对象存入一级缓存,替换三级缓存中的临时对象。

若没有代理需求,二级缓存(earlySingletonObjects)似乎可以直接存储原始对象,无需三级缓存。但问题在于:当 Bean 需要被代理时,必须确保注入的是代理对象而非原始对象


循环依赖 + 代理的复杂场景

假设 A 和 B 都需要被 AOP 代理(例如被 @Transactional 标记),此时若仅用二级缓存,会引发以下问题:

  1. A 的创建流程
    • 实例化 A(原始对象)。
    • 将 A 的原始对象存入二级缓存(earlySingletonObjects)。
    • 填充属性时发现依赖 B,开始创建 B。
  2. B 的创建流程
    • 实例化 B(原始对象)。
    • 填充 B 的属性时,从二级缓存获取 A 的原始对象(未代理)。
    • 完成 B 的初始化后,生成 B 的代理对象,存入一级缓存。
  3. 完成 A 的初始化
    • 在 A 的初始化后阶段(postProcessAfterInitialization),生成 A 的代理对象。
    • 最终缓存中的 A 是代理对象,但 B 中注入的 A 是原始对象,导致不一致

说实话,我自己都不能说服我自己。写的好勉强。。。这些疑问还是看这篇文章吧。【参考里面的第二篇文章】

end.参考

  1. https://mp.weixin.qq.com/s/dSRQBSG42MYNa992PvtnJA 【阿里云开发者 – 一文详解Spring Bean循环依赖】

  2. https://www.cnblogs.com/daimzh/p/13256413.html 【博客园 面试必杀技,讲一讲Spring中的循环依赖 】质量很高

  3. 原文链接:https://blog.csdn.net/chaitoudaren/article/details/105060882 【CSDN Spring源码最难问题】

相关文章:

spring详解-循环依赖的解决

Spring循环依赖 重点提示&#xff1a; 本文都快写完了&#xff0c;发现“丈夫” 的英文是husband… 在“②有AOP循环依赖” 改过来了&#xff0c;前面用到的位置太多了就没改。我是说怎么idea的hansband英文下面怎么有波浪线。各位能够理解意思就行&#xff0c;英文拼写不要过…...

【大模型面试每日一题】Day 10:混合精度训练如何加速大模型训练?可能出现什么问题?如何解决?

【大模型面试每日一题】Day 10&#xff1a;混合精度训练如何加速大模型训练&#xff1f;可能出现什么问题&#xff1f;如何解决&#xff1f; &#x1f4cc; 题目重现 &#x1f31f;&#x1f31f; 面试官&#xff1a;混合精度训练如何加速大模型训练&#xff1f;可能出现什么问…...

[学习]RTKLib详解:rtkcmn.c与rtkpos.c

文章目录 Part A、Rrtkcmn.c一、总体功能二、关键API列表三、核心算法实现四、函数功能与参数说明1. uniqnav2. lsq3. filter4. matmul5. satazel6. ionmapf7. geodist8. timeadd9. dgetrf_ / dgetri_&#xff08;LAPACK接口&#xff09; 五、工作流程说明4.1 模块在RTKLib中的…...

cookie/session的关系

什么是cookie&#xff0c;session 我们平时去医院看病时&#xff0c;从进医院那一刻&#xff0c;我们最开始要做的就是挂号&#xff08;需要我们填写表格&#xff0c;记录一些核心信息&#xff0c;医生会把这些信息录入电脑&#xff0c;并给我办一个就诊卡&#xff0c;卡里面只…...

Linux(十四)进程间通信(IPC),管道

一、进程间通信 &#xff08;一&#xff09;系统介绍进程间通信 进程间通信&#xff08;IPC&#xff09;介绍 小编插入的这篇文章详细介绍了进程间通信的一些内容&#xff0c;大家可以一起学习。 &#xff08;二&#xff09;进程间通信的方法 1、管道 2、信号量 3、共享…...

Nmap 工具的详细使用教程

Nmap&#xff08;Network Mapper&#xff09;是一款开源且功能强大的网络扫描和安全审计工具。它被广泛用于网络发现、端口扫描、操作系统检测、服务版本探测以及漏洞扫描等。 官方链接: Nmap 官方网站: https://nmap.org/Nmap 官方文档 (英文): https://nmap.org/book/man.h…...

Vue 自定义指令输入校验过滤

/*** 过滤字符串* param {*} filterCharRule* param {*} newVal* returns*/ function filterCharForValue(filterCharRule, newVal) {if(!filterCharRule || !newVal) returnconst isArray filterCharRule instanceof Arrayconst isRegExp filterCharRule instanceof RegExpi…...

OpenGl实战笔记(2)基于qt5.15.2+mingw64+opengl实现纹理贴图

一、作用原理 1、作用&#xff1a;将一张图片&#xff08;纹理&#xff09;映射到几何体表面&#xff0c;提升视觉真实感&#xff0c;不增加几何复杂度。 2、原理&#xff1a;加载图片为纹理 → 上传到 GPU&#xff1b;为顶点设置纹理坐标&#xff08;如 0~1 范围&#xff09;&…...

tinyrenderer笔记(透视矫正)

tinyrenderer个人代码仓库&#xff1a;tinyrenderer个人练习代码 引言 还要从上一节知识说起&#xff0c;在上一节中我为了调试代码&#xff0c;换了一个很简单的正方形 obj 模型&#xff0c;配上纹理贴图与法线贴图进行渲染&#xff0c;得了下面的结果&#xff1a; what&…...

c++类【发展】

类的静态成员&#xff08;用static声明的成员&#xff09;,在声明之外用例单独的语句进行初始化&#xff0c;初始化时&#xff0c;不再需要用static进行限定。在方法文件中初始化。以防重复。 特殊成员函数 复制构造函数&#xff1a; 当使用一个对象来初始化另一个对象…...

玛格丽特鸡尾酒评鉴,玛格丽特酒的寓意和象征

玛格丽特鸡尾酒会有独特的风味&#xff0c;而且还会有一个比较吸引人的背后故事。在目前的鸡尾酒界就会占据着很重要的地位&#xff0c;不仅是味蕾的盛宴&#xff0c;同样也会拥有深厚的情感。 玛格丽特由龙舌兰酒、柠檬汁和君度橙酒调制而成&#xff0c;将三者巧妙地结合在一起…...

关于Java多态简单讲解

面向对象程序设计有三大特征&#xff0c;分别是封装&#xff0c;继承和多态。 这三大特性相辅相成&#xff0c;可以使程序员更容易用编程语言描述现实对象。 其中多态 多态是方法的多态&#xff0c;是通过子类通过对父类的重写&#xff0c;实现不同子类对同一方法有不同的实现…...

SecureCrt设置显示区域横列数

1. Logical rows //逻辑行调显示区域高度的 一般超过50就全屏了 2. Logical columns //逻辑列调显示区域宽度的 3. Scrollback buffer //缓冲区大小...

【PhysUnits】1 SI Prefixes 实现解析(prefix.rs)

一、源码 // prefix.rs //! SI Prefixes (国际单位制词头) //! //! 提供所有标准SI词头用于单位转换&#xff0c;仅处理10的幂次 //! //! Provides all standard SI prefixes for unit conversion, handling only powers of 10.use typenum::{Z0, P1, P2, P3, P6, P9, P12, …...

【Python】--实现多进程

import multiprocessing import time # 1.定义好函数 # codeing def coding():for i in range(10):print(f正在编写第{i}行代码)time.sleep(0.2)# music def music():for i in range(10):print(f正在听第{i}首歌曲)time.sleep(0.2)单任务 # 单任务--时间为4s多 if __name__ _…...

计算机视觉与深度学习 | 基于数字图像处理的裂缝检测与识别系统(matlab代码)

🍅🍅🍅🍅🍅🍅🍅🍅🍅🍅🍅🍅🍅🍅🍅🍅 基于数字图像处理的裂缝检测与识别系统 🥦🥦🥦🥦🥦🥦🥦🥦🥦🥦🥦🥦🥦**系统架构设计****1. 图像预处理**目标:消除噪声+增强裂缝特征**2. 图像分割**目标:提取裂缝区域**3. 特征…...

嵌入式MCU语音识别算法及实现方案

在嵌入式MCU&#xff08;微控制器单元&#xff09;中实现语音识别&#xff0c;由于资源限制&#xff08;如处理能力、内存、功耗等&#xff09;&#xff0c;通常需要轻量级算法和优化技术。以下是常见的语音识别算法及实现方案&#xff1a; 一、传统语音识别算法 动态时间规整&…...

【C++核心技术深度解析:从继承多态到STL容器 】

一、C继承机制&#xff1a;代码复用与层次设计 1. 继承基础概念 什么是继承&#xff1f; 继承是面向对象编程的核心机制&#xff0c;通过class Derived : public Base让子类&#xff08;派生类&#xff09;复用父类&#xff08;基类&#xff09;的属性和方法&#xff0c;同时…...

【C/C++】new关键字解析

&#x1f4d8; C 中 new 关键字详解笔记 &#x1f539; 什么是 new&#xff1f; new 是 C 中用于动态内存分配的关键字&#xff0c;它在堆内存中为对象或变量分配空间&#xff0c;并返回对应类型的指针。 与 C 语言中的 malloc 相比&#xff0c;new 更安全、更方便&#xff…...

C++高性能内存池

目录 1. 项目介绍 1. 这个项目做的是什么? 2. 该项目要求的知识储备 2. 什么是内存池 1. 池化技术 2. 内存池 3. 内存池主要解决的问题 4.malloc 3. 先设计一个定长的内存池 4.高并发内存池 -- 整体框架设计 5. 高并发内存池 -- thread cache 6. 高并发内存池 -- …...

chili3d调试笔记12 deepwiki viewport

xiangechen/chili3d | DeepWiki viewport阅读 &#x1f9e0;deep 我要把模型投影成dxf导出有什么办法 引用lookat 截图是如何实现的 明天接着搞 ----------------------------------------------------------------...

前端取经路——JavaScript修炼:悟空的九大心法

大家好&#xff0c;我是老十三&#xff0c;一名前端开发工程师。JavaScript如同孙悟空的七十二变&#xff0c;变化多端却又充满威力。本篇文章我将带你攻克JS中最令人头疼的九大难题&#xff0c;从闭包陷阱到原型链继承&#xff0c;从异步编程到性能优化。每个难题都配有实战代…...

从零实战:在Xilinx Zynq PS端移植VxWorks 6.9系统

一、环境准备与工具链搭建 1.1 硬件配置清单 开发板: Zynq-7000系列(推荐ZedBoard或ZCU102)调试工具: USB-JTAG调试器(如Xilinx Platform Cable USB II)存储介质: SD卡(建议Class 10以上)1.2 软件环境 工具版本作用Vivado2022.1FPGA硬件设计Vitis2022.1系统集成开发Wind…...

网工实验——RIP配置

网络拓扑图 配置 1.为每台设备配置ip地址 AR4 <Huawei>u t m <Huawei>sys [Huawei]sysname AR4 [AR4]int g0/0/0 [AR4-GigabitEthernet0/0/0]ip address 172.16.1.1 24 [AR4-GigabitEthernet0/0/0]q#下面配置换回口&#xff0c;模拟网 [AR4]int LoopBack 0 [AR4…...

前端流行框架Vue3教程:14. 组件传递Props效验

(4) 组件传递Props效验 Vue组件可以更细致地声明对传入的props的校验要求 ComponentA.vue <script> import ComponentB from ./ComponentB.vue; export default {components: {ComponentB},data() {return {title: 标题}} } </script> <template><h3&g…...

电子电器架构 --- 网关ECU中采用多CPU解决方案来实现网关功能

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…...

关于tftpboot的用法

TFTPBOOT 是一个常用于嵌入式系统或网络设备中的命令&#xff0c;用于通过 TFTP 协议从网络上启动操作系统镜像或引导文件。这个命令通常在设备启动时执行&#xff0c;允许设备通过网络从 TFTP 服务器下载启动镜像或其他必要的文件&#xff0c;而不需要从本地存储中启动。 一般…...

团队协作的润滑剂——GitHub与协作流程

各位代码界的社交恐惧症患者们&#xff0c;今天我们要聊的是如何假装自己很会团队协作——使用GitHub&#xff01;这就像程序员版的"相亲平台"&#xff0c;只不过在这里&#xff0c;你展示的不是自拍和收入&#xff0c;而是代码和commit记录&#xff08;后者往往更令…...

数据库复习

DML操作包括&#xff1a; SELECT INSERT UPDATE DELETE MERGE 返回字符串长度&#xff1a;length() 查询记录&#xff1a;SELECT 增&#xff08;INSERT&#xff09;、删&#xff08;DELETE&#xff09;、改&#xff08;UPDATE&#xff09;、查&#xff08;SELECT&#…...

AI与机器学习、深度学习在气候变化预测中的应用与实践

前言&#xff1a; 全球气候变化是现代社会面临的最重要的环境挑战之一&#xff0c;影响了气温、降水、海平面、农业、生态系统等多个方面。气候变化的驱动因素主要包括温室气体排放、气溶胶浓度、火灾频发、海冰融化、叶绿素变化、农业变化和生态环境变化等。这些因素在全球范围…...

Laravel 12 基于 EMQX 实现 MQTT 消息发送与接收

Laravel 12 基于 EMQX 实现 MQTT 消息发送与接收 要在 Laravel 12 中实现基于 EMQX 的 MQTT 消息发送与接收&#xff0c;你可以按照以下步骤操作&#xff1a; 1. 安装必要的依赖包 首先安装 MQTT 客户端库&#xff1a; composer require php-mqtt/client2. 配置 EMQX 连接 …...

论广告系统对存算分离架构的应用

辅助论点 辅助论点一&#xff1a;存算分离架构起源于数据库领域&#xff0c;并不是在线系统。 存算分离的架构源于Google的Spanner数据库&#xff0c;这个数据库采用了KV做存储层&#xff0c;OLAP做计算层的分离式设计&#xff0c;其目的是能快速伸缩计算资源&#xff0c;且节…...

create-vue搭建Vue3项目(Vue3学习2)

一、认识create-vue image.png 二、create-vue搭建Vue3项目 image.png image.png 依次执行npm install 和npm run dev即可运行项目 image.png image.png © 著作权归作者所有,转载或内容合作请联系作者 喜欢的朋友记得点赞、收藏、关注哦&#xff01;&#xff01;&#xff…...

NHDEEP档案管理系统功能介绍

NHDEEP档案管理系统单机版专注于提高档案管理效率&#xff0c;无需网络连接即可独立运作&#xff0c;确保数据的安全与私密性。无论是机关单位的常规档案工作&#xff0c;还是工程、基建项目的特殊档案管理需求&#xff0c;系统都能提供全面的解决方案。系统支持信创环境。 核心…...

【C++】C++中的命名/名字/名称空间 namespace

C中的命名/名字/名称空间 namespace 1、问题引入2、概念3、作用4、格式5、使用命名空间中的成员5.1 using编译指令&#xff08; 引进整个命名空间&#xff09; ---将这个盒子全部打开5.2 using声明使特定的标识符可用(引进命名空间的某个成员) ---将这个盒子中某个成员的位置打…...

游戏引擎学习第260天:在性能分析器中实现钻取功能

昨天那个帧内存满之后触发段错误实在没找到什么原因导致的 继续研究一下为什么导致的 内存不够进来释放frame 释放frame 应该会给DebugState->FirstFreeStoredEvent 赋值吧 这段宏定义&#xff1a; #define FREELIST_DEALLOCATE(Pointer, FreeListPointer) \if(Pointer) {…...

人工智能100问☞第15问:人工智能的常见分类方式有哪些?

目录 一、通俗解释 二、专业解析 三、权威参考 人工智能的常见分类方式包括:​​按智能水平​​(弱人工智能、通用人工智能、超级人工智能)、​​按技术原理​​(生成式AI、判别式AI、强化学习)、​​按功能目标​​(生成内容、优化决策)、​​按应用领域​​(自然语…...

JavaSE核心知识点01基础语法01-04(数组)

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 JavaSE核心知识点01基础语法01-04&#xff0…...

抖音代播领航者——品融电商(PINKROON)的运营实力与服务解析

抖音代播领航者——品融电商&#xff08;PINKROON&#xff09;的运营实力与服务解析 在兴趣电商高速发展的背景下&#xff0c;杭州品融品牌管理有限公司&#xff08;PINKROON&#xff09;凭借其全域增长方法论与抖音生态的深度布局&#xff0c;成为众多品牌首选的抖音代播服务商…...

LeetCode 790 多米诺和托米诺平铺 题解

对于本题不去看LeetCode的评论区和题解很难想到如何去dp&#xff0c;毕竟就算再怎么枚举也很难找到适用于面向结果的规律。所以对于题解我建议大家还是去看一下灵神给的题解&#xff0c;以下是灵神汇总的图&#xff0c;如果能看懂的话&#xff0c;对于解决题目有很大的帮助。 根…...

力扣-hot100 (缺失的第一个正数)

41. 缺失的第一个正数 困难 给你一个未排序的整数数组 nums &#xff0c;请你找出其中没有出现的最小的正整数。 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,0] 输出&#xff1a;3 解释&#xff…...

Electrolink信息泄露(CVE-2025-28228)

免责声明 本文档所述漏洞详情及复现方法仅限用于合法授权的安全研究和学术教育用途。任何个人或组织不得利用本文内容从事未经许可的渗透测试、网络攻击或其他违法行为。使用者应确保其行为符合相关法律法规,并取得目标系统的明确授权。 对于因不当使用本文信息而造成的任何直…...

Leetcode Hot 100 三数之和

思路 对数组先排序&#xff0c;然后使用双指针法进行&#xff0c;并且整个过程需要把握去重的逻辑 代码 class Solution:def threeSum(self, nums: List[int]) -> List[List[int]]:if not nums:return []nums.sort() #去重都需要排序res[]for i in range(len(nums)):if i…...

5月6日日记

一点心得是 看通知要仔细认真&#xff0c;自己想问的问题要先看看通知或者文件中说了没有&#xff0c;如果没说再去问相关负责人。 上课的教室一定要看好&#xff0c;看准了再去。别像今天一样先去了科技楼又去了工学馆。 线代开课了。感觉总体还行&#xff0c;并不是很难。…...

巧记英语四级单词 Unit7-中【晓艳老师版】

collapse v.倒塌&#xff0c;坍塌 都(col)来扑(lap)噻immune a.不受影响的&#xff0c;免疫的 我im 木讷mune&#xff0c;人应该木讷一点yard n.院子 鸭的&#xff0c;在哪养&#xff1b;backyard 后院backward a.往后的 ward表示方向 profile n.外形&#xff0c;轮廓 从前面看…...

Windows系统修改Docker Desktop(WSL2)内存分配

# Windows查看docker信息 docker info 新增wsl全局配置文件(.wslconfig文件)&#xff1a;windows路径栏输入&#xff1a;%UserProfile%&#xff0c;找到目录C:\Users\Administrator&#xff0c;默认是没有这个配置文件的&#xff0c;可以自己新增 # 设置在wsl2上运行 [wsl2] # …...

Oracle02-安装

零、文章目录 Oracle02-安装 1、Windows Server2022安装Oracle11g &#xff08;1&#xff09;下载 百度网盘地址&#xff1a; https://pan.baidu.com/s/15MBkMt1ldbSFm4L74h7Myg?pwd8888下载完成两个压缩包解压放在一起 &#xff08;2&#xff09;安装 双击 setup 文件安…...

Linux[Makefile]

Makefile基础结构 规则语法 target: prerequisitescommandtarget&#xff1a;生成的目标&#xff08;如可执行文件、.o文件&#xff09; prerequisites&#xff1a;依赖项&#xff08;源码、头文件等&#xff09; command&#xff1a;构建命令&#xff08;必须用Tab缩进&am…...

相同的数(简单)

深度优先搜索 如果两个二叉树都为空&#xff0c;则两个二叉树相同。如果两个二叉树中有且只有一个为空&#xff0c;则两个二叉树一定不相同。 如果两个二叉树都不为空&#xff0c;那么首先判断它们的根节点的值是否相同&#xff0c;若不相同则两个二叉树一定不同&#xff0c;…...

「Mac畅玩AIGC与多模态22」开发篇18 - 多段输出拼接与格式化展现工作流示例

一、概述 本篇以已有多字段输出为基础&#xff0c;介绍如何通过执行 LLM 节点对多个上游字段进行统一拼接与格式化处理。开发人员将学习如何从多个节点输出中提取数据字段&#xff0c;并组合为结构清晰、风格统一的最终输出&#xff0c;提升用户阅读体验。 二、环境准备 mac…...