编程语言
首页 > 编程语言> > 源码解读Spring如何解决循环依赖

源码解读Spring如何解决循环依赖

作者:互联网

目录

前言

循环依赖的解释

源码解读前的准备

正文

hello world的代码

源码解读

Spring解决循环依赖的缓存图

Spring解决循环依赖的流程图

总结

相应课程的推荐


前言

循环依赖的解释

大家在面试中可能会遇到面试官问你Spring在内部它是如何解决循环依赖的?甚至可能有些小伙伴有看过Spring中IOC启动源码但是没去考虑循环依赖的问题,导致也没回答出这个问题!

那么循环依赖他到底是怎么样的一个现象呢?

假如你有两个类,一个A,一个B

在A中维护了B,B中维护了A

我们知道Java中类中赋值的操作基本都是构造方法或者是get和set方法来赋值(还有静态代码块之类的这里不提)。

那么我们先用Java构造方法来举个例

new A(new B(new A(new B(new A(new B)))))...............咱就是写到宇宙爆炸也写不完,这样就出现了死循环,也就是循环依赖。

在Spring中默认是生产的单例bean也就是只会生产一次,全局都使用这一个bean对象。那么他肯定是会使用到缓存来存这些生产好的对象的。并且Spring中提倡使用set方法来赋值。所以Spring内部也是靠set方法赋值和三级缓存来解决缓存依赖的问题。

源码解读前的准备

我们知道在程序中出现问题最好的解决办法就是看源码,那么我们追循环依赖的源码要预先准备一些啥呢?

1. 最好已经明白Spring中IOC的启动流程,因为循环依赖的解决是在IOC启动代码中。因为这篇帖子很多操作只会带过。如有不明白的同学可以看博主的IOC启动流程源码分析。

Spring中IOC源码解读https://blog.csdn.net/qq_43799161/article/details/122371976?spm=1001.2014.3001.5501

2. 大概明白IOC容器的启动流程后,就是编写好一段hello world代码(可以复制正文的),并且hello world代码中一定要打上日志,因为追源码当你很陌生的时候就是通过控制台输出日志来找准那行执行重要的方法!

3. 编写好hello world代码后就是熟知Idea的debug工具。因为全程肯定是要靠打断点的方式来追。

4. 上面的流程都准备好后,就是调整好状态出发!

注:其实任何的源码都是以这种方式来追,这就是一种思想。追一个源码不就是先大概懂它的一个执行流程和api,然后准备好一段hello world代码,然后使用idea的debug工具来追。

正文

hello world的代码

为了预防一些“懒鬼”我还是把hello world代码准备好把....

// 类A
/**
 * @Author liha
 * @Date 2021-09-09 14:15
 * 李哈YYDS
 */
public class BeanA {

    private BeanB b;

    public BeanB getB() {
        return b;
    }

    public void setB(BeanB b) {
        System.out.println("给变量b赋值");
        this.b = b;
    }

    public BeanA(){
        System.out.println("A无参构造...");
    }
}




// 类B
/**
 * @Author liha
 * @Date 2021-09-09 14:15
 * 李哈YYDS
 */
public class BeanB {


    private BeanA a;


    public BeanA getA() {
        return a;
    }

    public void setA(BeanA a) {
        System.out.println("给变量A赋值");
        this.a = a;
    }

    public BeanB(){
        System.out.println("B无参构造");
    }

}





// 启动类main方法
        /*
        * 找到配置文件
        * 注意这里要填写自己xml的位置
        * */
        ApplicationContext context = new ClassPathXmlApplicationContext("file:D:\\test1\\src\\test\\java\\com\\example\\test1\\spring5For\\bean1.xml");



        BeanA beanA =  context.getBean("beanA",BeanA.class);
        BeanB beanB =  context.getBean("beanB",BeanB.class);

        System.out.println(beanA);
        System.out.println(beanB);




// xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="beanA" class="com.example.test1.spring5For.BeanA">
        <property name="b" ref="beanB"></property>
    </bean>


    <bean id="beanB" class="com.example.test1.spring5For.BeanB">
        <property name="a" ref="beanA"></property>
    </bean>
    
</beans>

源码解读

再最后啰嗦一句,很多流程我只会截图并不会仔细讲解,我默认大家是明白IOC的启动流程。        

 此时就可以进入到createBean()方法中。

重要的内容都在createBean()方法中,但是进去之前我觉得我把之前的截图大概讲解一下吧

大致就是解析xml,在spring中一个xml配置文件中的bean就是一个BeanDefintion对象。所以大家可以在DefaultListableBeanFactory类中定义了beanDefinitionMap一个ConcurrentHashMap,并且通过registerBeanDefinition()添加到集合中。

解析完xml就是解析前后置操作BeanPostProcesser,比如aop切面操作。

然后就是来创建bean对象,创建前查询缓存,缓存中没得就创建。

下面就来到创建方法的具体


	protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
			throws BeanCreationException {

		// Instantiate the bean.
		BeanWrapper instanceWrapper = null;
		if (mbd.isSingleton()) {
			instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
		}
		if (instanceWrapper == null) {
			instanceWrapper = createBeanInstance(beanName, mbd, args);
		}
		Object bean = instanceWrapper.getWrappedInstance();
		Class<?> beanType = instanceWrapper.getWrappedClass();
		if (beanType != NullBean.class) {
			mbd.resolvedTargetType = beanType;
		}

		// Allow post-processors to modify the merged bean definition.
		synchronized (mbd.postProcessingLock) {
			if (!mbd.postProcessed) {
				try {
					applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
				}
				catch (Throwable ex) {
					throw new BeanCreationException(mbd.getResourceDescription(), beanName,
							"Post-processing of merged bean definition failed", ex);
				}
				mbd.postProcessed = true;
			}
		}

		// Eagerly cache singletons to be able to resolve circular references
		// even when triggered by lifecycle interfaces like BeanFactoryAware.
		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) {
			if (logger.isTraceEnabled()) {
				logger.trace("Eagerly caching bean '" + beanName +
						"' to allow for resolving potential circular references");
			}
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}

		// Initialize the bean instance.
		Object exposedObject = bean;
		try {
			populateBean(beanName, mbd, instanceWrapper);
			exposedObject = initializeBean(beanName, exposedObject, mbd);
		}
		catch (Throwable ex) {
			if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
				throw (BeanCreationException) ex;
			}
			else {
				throw new BeanCreationException(
						mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
			}
		}

		if (earlySingletonExposure) {
			Object earlySingletonReference = getSingleton(beanName, false);
			if (earlySingletonReference != null) {
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
				}
				else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
					String[] dependentBeans = getDependentBeans(beanName);
					Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
					for (String dependentBean : dependentBeans) {
						if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
							actualDependentBeans.add(dependentBean);
						}
					}
					if (!actualDependentBeans.isEmpty()) {
						throw new BeanCurrentlyInCreationException(beanName,
								"Bean with name '" + beanName + "' has been injected into other beans [" +
								StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
								"] in its raw version as part of a circular reference, but has eventually been " +
								"wrapped. This means that said other beans do not use the final version of the " +
								"bean. This is often the result of over-eager type matching - consider using " +
								"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
					}
				}
			}
		}

		// Register bean as disposable.
		try {
			registerDisposableBeanIfNecessary(beanName, bean, mbd);
		}
		catch (BeanDefinitionValidationException ex) {
			throw new BeanCreationException(
					mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
		}

		return exposedObject;
	}

代码量确实很多,不过看重点方法!

// 实例化对象,也就是反射反射创建对象,会执行构造方法
instanceWrapper = createBeanInstance(beanName, mbd, args);


// 添加到三级缓存中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));


// set方法给对象赋值
populateBean(beanName, mbd, instanceWrapper);


// 对象初始化的一些操作,比如实现BeanPostProcessor重写的方法之类的。还有init-method之类的。
exposedObject = initializeBean(beanName, exposedObject, mbd);

前面有说过循环依赖是通过三级缓存和set方法赋值来解决。

所以我们添加到三级缓存的方法addSingletonFactory()

	protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
		Assert.notNull(singletonFactory, "Singleton factory must not be null");
		synchronized (this.singletonObjects) {
			if (!this.singletonObjects.containsKey(beanName)) {
				this.singletonFactories.put(beanName, singletonFactory);
				this.earlySingletonObjects.remove(beanName);
				this.registeredSingletons.add(beanName);
			}
		}
	}

在这里我建议大家可以使用QQ截图的钉板功能将三级缓存的三个map集合给钉在桌面或者是使用idea的多窗功能,个人建议是钉板功能因为源码本来就比较脑袋疼开多窗真的脑袋疼~

三级缓存在DefaultSingletonBeanRegistry类中

// 一级   存放完全实例化好的bean对象
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 三级    存放bean工程对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

// 二级    存放半成品bean对象
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

继续看到addSingletonFactory()方法中这里就是把beanA给添加到三级缓存中。

 

然后继续往populateBean()方法追,这里就是重点。

// 前面的内容不多讲直接来到核心方法。给这个方法来一个断点,并且进入
if (pvs != null) {
			applyPropertyValues(beanName, mbd, bw, pvs);
}

直接来到最后一行,前面的内容不多讲直接来到核心方法。给这个方法来一个断点,并且进入

// 直接进入到for循环中的这一行
Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);

这里再给上一个断点并且进入

进入到resolveValueIfNecessary()方法中可以看到很多if和elseif,这里其实也就是解析你的xml中<bean>标签的<property>标签中使用了那些属性,比如ref之类的。而万物皆对象的Java肯定是一个属性对应一个实体类。我们知道一条<bean>对应一个BeanDefinition,一个<property>对应一个PropertyValue。而我们这里是循环依赖所以使用的ref,在spring中ref对RuntimeBeanReference。

		// 所以第一个if就成功进入,我们继续给resolveReference打一个断点追进去。
        if (value instanceof RuntimeBeanReference) {
			RuntimeBeanReference ref = (RuntimeBeanReference) value;
			return resolveReference(argName, ref);
		}

所以第一个if就成功进入,我们继续给resolveReference打一个断点追进去。

 因为我们这里toParent为false所以看到我们的else代码块中

else {
				String resolvedName;
				if (beanType != null) {
					NamedBeanHolder<?> namedBean = this.beanFactory.resolveNamedBean(beanType);
					bean = namedBean.getBeanInstance();
					resolvedName = namedBean.getBeanName();
				}
				else {
					resolvedName = String.valueOf(doEvaluate(ref.getBeanName()));
					bean = this.beanFactory.getBean(resolvedName);
				}
				this.beanFactory.registerDependentBean(resolvedName, this.beanName);
			}

else代码块中也就是把ref中的beanName取出来,然后就进行执行getBean()方法,追过ioc的同学应该对getBean()不能再熟悉了。我们知道beanA中维护了B,所以这里就是去创建beanB

那么继续往getBean()方法中追进去 

getBean()中无非就是先查缓存,缓存中没得就创建,就直接看到创建的逻辑把。这里就快进了

 又来到熟悉doCreateBean()方法了,这里就是实例化beanB,将beanB添加到三级缓存中。

目前beanA和beanB都进入到三级缓存中了,接下来又来到populateBean()方法,而我们这次给beanB中的属性赋值,那就是给beanA赋值。那么接下来也快进了。

 这种繁琐同样的操作我们就可以从我们前面打的断点中直接放行。

 再放行就又到了我们熟悉的getBean()方法了。

这里我们回忆一下,目前我们beanA在赋值过程中发现引用了beanB所以创建了beanB,而beanB在创建中又发现引用了beanA所以现在又到达了beanA的getBean()方法。

这里我挺担心大伙可能文字阅读能力或者是博主的文字表达能力不行,所以特意画了张图。

 目前这张图对应之前全部的操作,但是并非是全部的操作。

这里再啰嗦一句,getBean()方法无非就是查缓存没缓存就创建,之前我们beanA、beanB都没有缓存所以是创建,但是我们现在beanA和beanB都是在三级缓存中,所以我们继续往下追。

进入到doGetBean()中

		String beanName = transformedBeanName(name);
		Object bean;

		// Eagerly check singleton cache for manually registered singletons.
		Object sharedInstance = getSingleton(beanName);
		if (sharedInstance != null && args == null) {
			if (logger.isTraceEnabled()) {
				if (isSingletonCurrentlyInCreation(beanName)) {
					logger.trace("Returning eagerly cached instance of singleton bean '" + beanName +
							"' that is not fully initialized yet - a consequence of a circular reference");
				}
				else {
					logger.trace("Returning cached instance of singleton bean '" + beanName + "'");
				}
			}
			bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
		}

 都说getBean()都是先查缓存,所以哪里是查缓存呢,没错就是在getSingleton(beanName)方法中。之前因为没有缓存就直接跳过了,那么现在就追进去。

@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// Quick check for existing instance without full singleton lock
		Object 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 lock
					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;
	}

 1个if、2个if、3个if、4个if、5个if,期初我以为这里肯定是逻辑层层,其实这里逻辑还是蛮简单的。

其实就是先查一级、一级没有就查二级,二级没有就上个同步锁,然后double check再查一遍一级、一级没有就再查二级、二级没有就查三级。

三级缓存中有存入beanA和beanB,所以直接看到最里面的if代码块

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);
							}
						}

看到singletonFactory.getObject(); 此时我们再看到添加三级缓存中方法。

这里就是一个lambda表达式,是在取缓存的时候一个回调,返回我们的一个beanA对象。

这里就是从三级缓存中取出beanA,然后将beanA添加到二级缓存中,我们知道二级缓存中存的是半成品,所以这个半成品就可以理解为实例化了,但是没有赋值。然后再从三级缓存中remove掉,并且此方法的放回值就是接口回调的返回值,也就是beanA。那我们接着往后走。

所以我们目前的缓存如下图。

 

接着往下走,在doGetBean()中取出缓存并且返回bean对象

 又回到了这里,此时我们这里是beanB的赋值过程,然而这里已经获取到beanB所引用的对象beanA,接着往下走。

一路的return就回到了AbstractAutowireCapableBeanFactory中applyPropertyValues()方法了。我们接着往下走。

这里是一个for循环,然而我们的beanB中只有一个<Property>标签,而我们前面的操作都是循环类的。就是从缓存中获取到beanA,所以循环一次就退出了,咱们接着往下走。

try {
	bw.setPropertyValues(new MutablePropertyValues(deepCopy));
}

 来到1729行的的try代码块中,这里就是将前面获取到的缓存中的beanA对象赋值给beanB维护的beanA对象。这里讲解一下在这之前的一些操作干了些啥把,其实就是将取出来的beanA对象做了一层包装,保存在一个deepCopy的ArrayList<PropertyValue>数组中。然后就是赋值了。

赋完值以后就回到了doCreateBean()方法中。initializeBean方法就是做一些初始化的操作,如果你的一些init-method方法,比如你的BeanPostPrecoessor重写的方法,比如Aware系列的接口重写的方法,比如重写InitializingBean接口的方法。一些执行,对这些接口的前后顺序想要了解的小伙伴可以追进去了解一下。但是这里并不是我们的核心,所以我们接着往下走。

if (earlySingletonExposure) {
			Object earlySingletonReference = getSingleton(beanName, false);
			if (earlySingletonReference != null) {
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
				}
				else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
					String[] dependentBeans = getDependentBeans(beanName);
					Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
					for (String dependentBean : dependentBeans) {
						if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
							actualDependentBeans.add(dependentBean);
						}
					}
					if (!actualDependentBeans.isEmpty()) {
						throw new BeanCurrentlyInCreationException(beanName,
								"Bean with name '" + beanName + "' has been injected into other beans [" +
								StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
								"] in its raw version as part of a circular reference, but has eventually been " +
								"wrapped. This means that said other beans do not use the final version of the " +
								"bean. This is often the result of over-eager type matching - consider using " +
								"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
					}
				}
			}
		}

这段代码对于beanB没有任何的处理,但是等等beanA会有处理。因为这里是判断你当前的这个bean对象是否存在于二级缓存中。

那我们继续往下走

// 这里也就是看你是否实现了DisposableBean接口,DisposableBean接口是在摧毁前回调的。
try {
	registerDisposableBeanIfNecessary(beanName, bean, mbd);
}

 这里也就是看你是否实现了DisposableBean接口,DisposableBean接口是在摧毁前回调的。不是我们的核心,我们继续往下走。

 继续往下走

if (newSingleton) {
	addSingleton(beanName, singletonObject);
}

 因为在前面给newSingleton赋值了true,所以这里执行addSingleton()方法,我们追进去。

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);
		}
	}

可以看出这里把beanB添加到一级缓存中,并且把beanB的二级缓存和三级缓存给remove了。

 此时beanB已经是成品了。我们继续往下走,因为我们创建beanB其实是在beanA的赋值操作中,所以其实这就是递归的一个过程有没有发现。

 注意这是一个递归往上走的操作,我们现在是往beanA的赋值操作走,getBean()是不是很熟悉,就是beanA赋值过程中的那个getBean(),而且我们此时的返回值就是beanB。继续往下走。

 又达到AbstractAutowireCapableBeanFactory中applyPropertyValues()方法了。此时我们在beanB的赋值过程已经经历一遍流程了,所以我们进入到try代码块。

try {
	bw.setPropertyValues(new MutablePropertyValues(deepCopy));
}

这就是赋值过程,在这之前我们返回了beanB对象,并且对它做了一层包装, 并且放入到deepCopy的ArrayList<PropertyValue>数组中。

 最终我们总算是回到了beanA的doCreateBean()方法了。继续往下走,马上追完了。

if (earlySingletonExposure) {
			Object earlySingletonReference = getSingleton(beanName, false);
			if (earlySingletonReference != null) {
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
				}
				else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
					String[] dependentBeans = getDependentBeans(beanName);
					Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
					for (String dependentBean : dependentBeans) {
						if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
							actualDependentBeans.add(dependentBean);
						}
					}
					if (!actualDependentBeans.isEmpty()) {
						throw new BeanCurrentlyInCreationException(beanName,
								"Bean with name '" + beanName + "' has been injected into other beans [" +
								StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
								"] in its raw version as part of a circular reference, but has eventually been " +
								"wrapped. This means that said other beans do not use the final version of the " +
								"bean. This is often the result of over-eager type matching - consider using " +
								"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
					}
				}
			}
		}

前面有说过这段if代码块对于beanB没有作用,但是对于beanA还是挺有作用的,目前我们的beanA在二级缓存中。所以这段代码就是从二级缓存中取到beanA对象。

从二级缓存中获取到beanA对象后做完doCreateBean()方法返回值。继续往下走!

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);
		}
	}

跟beanB一样的操作就是将经历过全部工程的bean对象从二级缓存或者是三级缓存中放入到一级缓存中,并且将二级缓存和三级缓存中删除。

Spring解决循环依赖的缓存图

目前的缓存图如下:

全部的创建过程就在此了。 后面就是beanA的一路返回,并且之前还有一个循环记得么,就是将BeanDefinition给遍历,因为我们有xml中有2条xml,一个是beanA,一个是beanB,但是我们的beanB已经在一级缓存中了,所以getBean()方法就直接从缓存中获取返回就行。到此所有的流程就全部已经追完了。

Spring解决循环依赖的流程图

全部的流程图如下:

 

总结

对于平常的面试来说,能问上循环依赖的这个问题,代表您这场面试的质量不会低。其实循环依赖这个问题考的是您对于Spring中IOC的一个启动流程是否有仔仔细细的追过,而且还要考虑到比较极端的情况。

相应课程的推荐

对于一个这么复杂的源码来说,用口头来表达情况肯定是很难的,因为每个人的思维不一样,除非是帖子写的特别的有质量。但是博主也是才写帖不久,表达能力啥的都待提升。所以博人给你们推荐了不错的课程来辅助理解(课程都是免费,而且都是博主觉得特别质量的课程)!

Spring解决循环依赖底层源码解读之视频讲解icon-default.png?t=M0H8https://www.bilibili.com/video/BV1zb411M7NQ?p=152Spring中IOC容器启动步骤源码解读之视频讲解icon-default.png?t=M0H8https://www.bilibili.com/video/BV1uE411C7CW?p=96

对Spring了解不够深的同学建议先听下面链接的IOC启动步骤,再去听Spring解决循环依赖。

博主写帖子的目的就是让大家能免费学习,博主也是为代码开源化做出一份贡献!但是目前博主能力有限所以会在总结的时候给大家推荐博主觉得很不错的相应课程,大家可以一半视频一半帖子结合来学习!

最最最最最后觉得博主能对您的学习或者思想有帮助的话麻烦给个关注+点赞把!!!!

标签:缓存,beanB,beanA,mbd,Spring,beanName,解读,bean,源码
来源: https://blog.csdn.net/qq_43799161/article/details/122724040