Spring源码深度解析:十一、Spring的循环依赖

news2024/11/15 9:52:19

一、前言

文章目录:Spring源码深度解析:文章目录

这篇文章是接着 Spring源码深度解析:七、bean的加载① - doGetBean 的继续分析过程。

二、什么是循环依赖

循环依赖,其实就是循环引用,就是两个或者两个以上的 bean 互相引用对方,最终形成一个闭环,如 A 依赖 B,B 依赖 C,C 依赖 A。如下图所示:
在这里插入图片描述
循环依赖,其实就是一个死循环的过程,在初始化 A 的时候发现引用了 B,这时就会去初始化 B,然后又发现 B 引用 C,跑去初始化 C,初始化 C 的时候发现引用了 A,则又会去初始化 A,依次循环永不退出,除非有终结条件。

三种循环依赖的情况:

  • 构造器的循环依赖:这种依赖spring是处理不了的,直接抛出BeanCurrentlylnCreationException异常。
  • 单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖,能处理。
  • 非单例循环依赖:无法处理。原型(Prototype)的场景是不支持循环依赖的,通常会走到AbstractBeanFactory类中下面的判断,抛出异常。

下面详解每一种情况。

1 构造器循环依赖

表示通过构造器注入构成的循环依赖,这种依赖是无法解决的只能抛出 BeanCurrentlyInCreationException异常表示循环依赖。

如在创建A类时,构造器需要创建B类,则回去创建B类,在创建B类过程中又发现要去创造C类,又去创建C类,而 创建C类有需要A类。则会形成一个闭环,无法解决。

因此Spring 将每一个正在创建的bean标识符放在了一个“当前创建bean池”(singletonsCurrentlyInCreation 集合)。当bean标识符在创建过程中将一直保持在这个池中,,因此如果创建bean过程中发现自己已经在“当前创建bean池”中,则会抛出 BeanCurrentlyInCreationException异常,并将创建完毕的bean从“当前创建bean池”中清除。

2 setter 循环依赖

表示通过setter 注入方式构成的循环依赖。对于setter 注入造成的依赖是通过Spring容器提前暴露刚完构造器注入但并未完成其他步骤(如setter注入,即仅仅自己完成了创建,但是对里面引用的属性还未创建完成) 的bean来完成的。而且只能解决单例作用域的bean循环依赖。通过提前暴露一个单例工厂方法,从而使其他bean能引用到该bean。
如下代码所示:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

AbstractAutowireCapableBeanFactory#getEarlyBeanReference()

	 /**
	 * 给调用者一次机会,主要就是调用了SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference()方法。将getEarlyBeanReference方法的返回值作为提前暴露的对象。
	 * 我们可以通过实现getEarlyBeanReference()方法来替代Spring提前暴露的对象
	 * Aop就是在这里将Advice动态织入bean中,若没有bean则直接返回bean,不做任何处理
	 */
	protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
		Object exposedObject = bean;
		// 当前类并非合成类 && 在hasInstantiationAwareBeanPostProcessors中
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
			for (BeanPostProcessor bp : getBeanPostProcessors()) {
				if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
					SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
				}
			}
		}
		return exposedObject;
	}

具体步骤如下:

  1. Spring 容器创建单例 “testA” bean, 首先根据无参构造器创建bean,并暴露一个“ObjectFactory” 用于返回一个提前暴露一个创建中的bean,并将“testA” 标识符放到“当前创建bean池”,然后通过setter 注入 “testB”;
  2. Spring 容器创建单例“testB” bean,首先根据无参构造器创建bean,并暴露一个“ObjectFactory” 用于返回一个提前暴露一个创建中的bean,并将“testB” 标识符放到“当前创建bean池”,然后通过setter 注入 “testC”;
  3. Spring 容器创建单例“testC” bean,首先根据无参构造器创建bean,并暴露一个“ObjectFactory” 用于返回一个提前暴露一个创建中的bean,并将“testC” 标识符放到“当前创建bean池”,然后通过setter 注入 “testA”。进行诸如“testA”时由于提前暴露了“ObjectFactory” 工厂,从而使它返回提前暴露一个创建中的bean
  4. 最后再依赖注入“testB” 和“testA”,完成setter 注入。

3. prototype范围的依赖处理

对于 prototype 作用域的bean,Spring容器无法完成依赖注入,因为Spring容器中无法缓存prototype 作用域的bean,因此无法暴露一个创建中的bean。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ACircular {
    private BCircular bCircular;
    @Autowired
    public void setbCircular(BCircular bCircular) {
        this.bCircular = bCircular;
    }
}
...
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class BCircular {
    private ACircular aCircular;
    @Autowired
    public void setaCircular(ACircular aCircular) {
        this.aCircular = aCircular;
    }
}

调用时会抛出异常。注意这里是调用时,并非是服务启动时,因为原型模式的特性并不需要启动时就创建好bean。在调用的时候才会尝试创建,所以这里在使用bean 的时候才会抛出异常。
在这里插入图片描述

三、思路分析

要明白Spring中的循环依赖,得先明白Spring中Bean的生命周期。

Bean 的生成步骤

被 Spring 管理的对象叫做 Bean 。Bean的生成步骤如下:

  1. Spring 扫描 class 得到BeanDefinition
  2. 根据得到的 BeanDefinition去生成 bean;
  3. 首先根据 class 推断构造方法;
  4. 根据推断出来的构造方法,反射,得到一个对象==(暂时叫做原始对象)==;
  5. 填充原始对象中的属性(依赖注入);
  6. 如果原始对象中的某个方法被 AOP 了,那么则需要根据原始对象生成一个代理对象;
  7. 把最终生成的代理对象放入单例池==(源码中叫做 singletonObjects)==中,下次 getBean 时就直接从单例池拿即可;

对于 Spring 中的 Bean 的生成过程,步骤还是很多的,并且不仅仅只有上面的7步,还有很多很多,这里不详细说了。

我们可以发现,在得到一个原始对象后,Spring 需要给对象中的属性进行依赖注入,那么这个注入过程是怎样的?

比如,A 类中存在一个 B 类的 b 属性,所以,当 A 类生成了一个原始对象之后,就会去给 b 属性去赋值,此时就会根据 b 属性的类型和属性名去BeanFactory 中去获取 B 类所对应的单例bean。

  1. 如果此时 BeanFactory 中存在 B 对应的 Bean,那么直接拿来赋值给 b 属性;
  2. 如果此时 BeanFactory 中不存在 B 对应的 Bean,则需要生成一个 B 对应的 Bean,然后赋值给 b属性。

问题就出现在「第二种」情况,如果此时 B 类在 BeanFactory 中还没有生成对应的 Bean,那么就需要去生成,就会经过 B 的 Bean 的生命周期。

那么在创建 B 类的 Bean 的过程中,如果 B 类中存在一个 A 类的 a 属性,那么在创建 B 的 Bean 的过程中就需要 A 类对应的 Bean,但是,触发 B 类 Bean 的创建的条件是 A 类 Bean 在创建过程中的依赖注入,所以这里就出现了循环依赖:

A Bean创建–>依赖了 B 属性–>触发 B Bean创建—>B 依赖了 A 属性—>需要 A Bean(但A Bean还在创建过程中)

从而导致 A Bean 创建不出来,B Bean 也创建不出来。

上文分析得到,之所以产生循环依赖的问题

主要是:A创建时—>需要B---->B去创建—>需要A,从而产生了循环。
在这里插入图片描述
那么如何打破这个循环,加个缓存就可以了
在这里插入图片描述
A 的 Bean 在创建过程中,在进行依赖注入之前,先把 A 的原始 Bean 放入缓存(提早暴露,只要放到缓存了,其他 Bean 需要时就可以从缓存中拿了),放入缓存后,再进行依赖注入,此时 A 的Bean 依赖了 B 的 Bean 。

如果 B 的 Bean 不存在,则需要创建 B 的 Bean,而创建 B 的 Bean 的过程和 A 一样,也是先创建一个 B 的原始对象,然后把 B 的原始对象提早暴露出来放入缓存中, 然后在对 B 的原始对象进行依赖注入 A,此时能从缓存中拿到 A 的原始对象(虽然是 A 的原始对象,还不是最终的 Bean),B 的原始对象依赖注入完了之后,B 的生命周期结束,那么 A 的生命周期也能结束。

因为整个过程中,都只有一个 A 原始对象,所以对于 B 而言,就算在属性注入时,注入的是 A 原始对
象,也没有关系,因为A 原始对象在后续的生命周期中在堆中没有发生变化。

四、解决方案

在Spring中,通过某些机制帮开发者解决了部分循环依赖的问题,这个机制就是「三级缓存

  • 一级缓存为:singletonObjects;它是我们最熟悉的朋友,俗称“单例池”“容器”,缓存创建完成单例Bean的地方。
  • 二级缓存为:earlySingletonObjects;映射Bean的早期引用,也就是说在这个Map里的Bean不是完整的,甚至还不能称之为“Bean”,只是一个Instance.
  • 三级缓存为:singletonFactories; 映射创建Bean的原始工厂
    在这里插入图片描述
	//	一级缓存:用于保存BeanName和创建bean实例之间的关系,即缓存bean。 beanname -> instance
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	// 二级缓存:也是保存BeanName和创建bean实例之间的关系,与singletonObjects 不同的是,如果一个单例bean被保存在此,则当bean还在创建过程中(比如 A类中有B类属性,当创建A类时发现需要先创建B类,这时候Spring又跑去创建B类,A类就会添加到该集合中,表示正在创建),就可以通过getBean方法获取到了,其目的是用来检测循环引用。
	private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
	
	// 三级缓存:用于保存BeanName和常见bean的工厂之间的关系。beanname-> ObjectFactory
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

后两个Map其实是“垫脚石”级别的,只是创建Bean的时候,用来借助了一下,创建完成就清掉了。那么Spring 是如何通过上面介绍的三级缓存来解决循环依赖的呢?这里只用 A,B 形成的循环依赖来举例:

1. 实例化 A,此时 A 还未完成属性填充和初始化方法(@PostConstruct)的执行,A 只是一个半成品。

2.A 创建一个 Bean工厂,并放入到 singletonFactories 中。

3. 发现 A 需要注入 B 对象,但是一级、二级、三级缓存均为发现对象 B4. 实例化 B,此时 B 还未完成属性填充和初始化方法(@PostConstruct)的执行,B 只是一个半成品。
 
5.B 创建一个 Bean工厂,并放入到 singletonFactories 中。

6. 发现 B 需要注入 A 对象,此时在一级、二级未发现对象A,但是在三级缓存中发现了对象 A,从三级缓存中得到对象 A,并将对象 A 放入二级缓存中,同时删除三级缓存中的对象 A。(注意,此时的 A
还是一个半成品,并没有完成属性填充和执行初始化方法)

7. 将对象 A 注入到对象 B 中。

8. 对象 B 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 B。(此时对象 B 已经是一个成品)

9. 对象 A 得到对象B,将对象 B 注入到对象 A 中。(对象 A 得到的是一个完整的对象 B10. 对象 A完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 A

在这里插入图片描述
我们从源码的角度来看一下这个过程:
创建 Bean 的方法在AbstractAutowireCapableBeanFactory::doCreateBean()

	// 创建Bean的核心方法
	protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
			throws BeanCreationException {

		// 实例化Bean
		// Instantiate the bean.
		BeanWrapper instanceWrapper = null;
		if (mbd.isSingleton()) {
			// 有可能在本Bean创建之前,就有其他Bean把当前Bean给创建出来(比如依赖注入过程中)
			// 单例情况下清除缓存。这里保存的是 FactoryBean 和 BeanWrapper 的映射关系。
			// factoryBeanInstanceCache是在创建其他bean的时候缓存了一下FactoryBean 。
			instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
		}
		// 如果没有缓存,则重新创建
		if (instanceWrapper == null) {
			// 1. 创建Bean实例:根据指定的bean使用对应的策略创建新的实例。如:工厂方法、构造函数自动注入,简单初始化
			instanceWrapper = createBeanInstance(beanName, mbd, args);
		}
		// 获取bean实例
		Object bean = instanceWrapper.getWrappedInstance();
		// 获取bean类型
		Class<?> beanType = instanceWrapper.getWrappedClass();
		// 将目标类型替换成实际生成的类型.纠正了上面说到类型错误(如果存在)
		if (beanType != NullBean.class) {
			mbd.resolvedTargetType = beanType;
		}

		// 2. 调用 MergedBeanDefinitionPostProcessor 后处理器,后置处理合并后的BeanDefinition
		// Allow post-processors to modify the merged bean definition.
		synchronized (mbd.postProcessingLock) {
			if (!mbd.postProcessed) {
				try {
					// 调用MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition()后处理器的方法。
					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.
		// 3. 判断是否需要提早曝光:单例 & 允许循环依赖 & 当前bean已经正在创建中
		// 由于当前bean已经在创建中,本次创建必然是循环引用造成的,所以这里判断是否可以需要提前曝光
		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) {
			if (logger.isDebugEnabled()) {
				logger.debug("Eagerly caching bean '" + beanName +
						"' to allow for resolving potential circular references");
			}
			// 4. 为避免后期循环依赖,在bean初始化完成前将创建实例的ObjectFactory加入工程  -- 解决循环依赖:添加到三级缓存
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}

		// Initialize the bean instance.
		Object exposedObject = bean;
		try {
			// 5.对bean进行属性填充,将各个属性值注入,其中如果存在依赖于其他bean的属性,则会递归初始依赖bean
			populateBean(beanName, mbd, instanceWrapper);
			// 调用初始化方法,比如Aware接口的实现,init-method、InitializingBean属性等。
			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);
			}
		}

		// 6. 进行循环依赖检查
		if (earlySingletonExposure) {
			Object earlySingletonReference = getSingleton(beanName, false);
			// earlySingletonReference只有在检测到有循环依赖的情况下才会不为空
			if (earlySingletonReference != null) {
				// 如果exposedObject没有在初始化方法中被改变,也就是没有被增强
				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);
						}
					}
					// 因为bean创建后其所依赖的bean一定是已经创建了的。actualDependentBeans不为空说明当前bean创建后其依赖的bean却没有全部创建完,也就说说存在循环依赖。
					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 {
			// 7.根据Scopse 注册bean
			registerDisposableBeanIfNecessary(beanName, bean, mbd);
		}
		catch (BeanDefinitionValidationException ex) {
			throw new BeanCreationException(
					mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
		}

		return exposedObject;
	}

添加三级缓存的方法如下:

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

@FunctionalInterface
public interface ObjectFactory<T> {
	T getObject() throws BeansException;
}

通过这段代码,我们可以知道 Spring 在实例化对象的之后,就会为其创建一个 Bean 工厂,并将此工厂加入到三级缓存中。
因此,Spring 一开始提前暴露的并不是实例化的 Bean,而是将 Bean 包装起来的 ObjectFactory。为什么要这么做呢?
这实际上涉及到 AOP,如果创建的 Bean 是有代理的,那么注入的就应该是代理 Bean,而不是原始的 Bean。但是 Spring 一开始并不知道 Bean 是否会有循环依赖,通常情况下(没有循环依赖的情况下),Spring 都会在完成填充属性,并且执行完初始化方法之后再为其创建代理。但是,如果出现了循环依赖的话,Spring 就不得不为其提前创建代理对象,否则注入的就是一个原始对象,而不是代理对象。因此,这里就涉及到应该在哪里提前创建代理对象?
Spring 的做法就是在ObjectFactory中去提前创建代理对象。它会执行getObject() 方法来获取到 Bean。实际上,它真正执行的方法如下:

	/** 给调用者一次机会,主要就是调用了SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference()方法。将getEarlyBeanReference方法的返回值作为提前暴露的对象。
	 * 我们可以通过实现getEarlyBeanReference()方法来替代Spring提前暴露的对象
	 * Aop就是在这里将Advice动态织入bean中,若没有bean则直接返回bean,不做任何处理
	 */
	protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
		Object exposedObject = bean;
		// 当前类并非合成类 && 在hasInstantiationAwareBeanPostProcessors中
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
			for (BeanPostProcessor bp : getBeanPostProcessors()) {
				if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
					SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
				}
			}
		}
		return exposedObject;
	}

因为提前进行了代理,避免对后面重复创建代理对象,会在 earlyProxyReferences 中记录已被代理的对象。

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
		implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        // 记录已被代理的对象
        this.earlyProxyReferences.put(cacheKey, bean);
        return wrapIfNecessary(bean, beanName, cacheKey);
    }
}

通过上面的解析,我们可以知道 Spring 需要三级缓存的目的是为了在没有循环依赖的情况下,延迟代理对象的创建,使 Bean的创建符合 Spring 的设计原则。

  • 如何获取依赖
    我们目前已经知道了 Spring 的三级依赖的作用,但是 Spring 在注入属性的时候是如何去获取依赖的呢?
    他是通过一个getSingleton()方法去获取所需要的 Bean 的。
	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// 尝试从单例缓存(一级缓存) singletonObjects 中获取完整的Bean。
		Object singletonObject = this.singletonObjects.get(beanName);
		// 如果单例缓存(一级缓存)中没有对象,创建中的Bean的名字会被保存在singletonsCurrentlyInCreation中
		// 也就是说,当前所需要获取的bean是否是singleton的,并且处于创建中的形态
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			// 如果单例缓存中不存在该bean,则加锁进行接下来的处理
			// 这里作为锁还有一个原因是二级缓存和三级缓存都是HashMap,需要一个锁来控制这两个map的操作
			synchronized (this.singletonObjects) {
				// 尝试从二级缓存earlySingletonObjects中获取半成品的Bean, 则直接将singletonObject返回。
				// 二级缓存存储的是未对属性进行添加的Bean.
				singletonObject = this.earlySingletonObjects.get(beanName);
				// 如果还获取不到,并且allowEarlyReference为true,则表示可以进行循环引用
				if (singletonObject == null && allowEarlyReference) {
					// 从三级缓存singletonFactories这个ObjectFactory实例的缓存中尝试获取创建此Bean的单例工厂实例
					// ObjectFactory为用户定制(容器中的代理Bean),FactoryBean框架会进行特殊处理(自定义)
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						// 调用单例工厂的getObject方法获取对象实例
						singletonObject = singletonFactory.getObject();
						// 将实例放入二级缓存中.
						this.earlySingletonObjects.put(beanName, singletonObject);
						// 从三级缓存中删除
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}

当 Spring 为某个 Bean 填充属性的时候,它首先会寻找需要注入对象的名称,然后依次执行 getSingleton()方法得到所需注入的对象,而获取对象的过程就是先从一级缓存中获取,一级缓存中没有就从二级缓存中获取,二级缓存中没有就从三级缓存中获取,如果三级缓存中也没有,那么就会去执行doCreateBean()方法创建这个 Bean。
在这里插入图片描述

五、解决循环依赖必须要三级缓存吗

我们现在已经知道,第三级缓存的目的是为了延迟代理对象的创建,因为如果没有依赖循环的话,那么就不需要为其提前创建代理,可以将它延迟到初始化完成之后再创建。

既然目的只是延迟的话,那么我们是不是可以不延迟创建,而是在实例化完成之后,就为其创建代理对象,这样我们就不需要第三级缓存了。因此,我们可以将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)) { // 判断一级缓存中不存在此对象
            object o = singletonFactory.getObject(); // 直接从工厂中获取 Bean
            this.earlySingletonObjects.put(beanName, o); // 添加至二级缓存中
            this.registeredSingletons.add(beanName);
        }
    }
}

这样的话,每次实例化完 Bean之后就直接去创建代理对象,并添加到二级缓存中。测试结果是完全正常的,Spring的初始化时间应该也是不会有太大的影响,因为如果 Bean 本身不需要代理的话,是直接返回原始 Bean 的,并不需要走复杂的创建代理 Bean 的流程。

  • 结论

测试证明,二级缓存也是可以解决循环依赖的。为什么 Spring 不选择二级缓存,而要额外多添加一层缓存呢?

如果 Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。所以,Spring 选择了三级缓存。但是因为循环依赖的出现,导致了 Spring 不得不提前去创建代理,因为如果不提前创建代理对象,那么注入的就是原始对象,这样就会产生错误。

六、无法解决的循环依赖问题

  1. 在主bean中通过构造函数注入所依赖的bean。

如下controller为主bean,service为所依赖的bean:

@RestController
public class AccountController {
    private static final Logger LOG = LoggerFactory.getLogger(AccountController.class);

    private AccountService accountService;

    // 构造函数依赖注入
    // 不管是否设置为required为true,都会出现循环依赖问题
    @Autowire
    // @Autowired(required = false)
    public AccountController(AccountService accountService) {
        this.accountService = accountService;
    }
    
}

@Service
public class AccountService {
    private static final Logger LOG = LoggerFactory.getLogger(AccountService.class);
    
    // 属性值依赖注入
    @Autowired
    private AccountController accountController;
   } 

启动打印如下:

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  accountController defined in file [D:\IDEA\learnspace\spring-framework\spring-debug\src\main\java\com\wts\FactoryBeanTest\AccountController.java]
↑     ↓
|  accountService (field private com.wts.FactoryBeanTest.AccountController com.wts.FactoryBeanTest.AccountService.accountController)
└─────┘

如果是在主bean中通过属性值或者setter方法注入所依赖的bean,而在所依赖的bean使用了构造函数注入主bean对象,这种情况则不会出现循环依赖问题。

@RestController
public class AccountController {
    private static final Logger LOG = LoggerFactory.getLogger(AccountController.class);

    // 属性值注入
    @Autowired
    private AccountService accountService;
    
}

@Service
public class AccountService {
    private AccountController accountController;

    // 构造函数注入
    @Autowired
    public AccountService(AccountController accountController) {
        this.accountController = accountController;
    }
}
  1. 总结
  • 当存在循环依赖时,主bean对象不能通过构造函数的方式注入所依赖的bean对象,而所依赖的bean对象则不受限制,即可以通过三种注入方式的任意一种注入主bean对象。
  • 如果主bean对象通过构造函数方式注入所依赖的bean对象,则无论所依赖的bean对象通过何种方式注入主bean,都无法解决循环依赖问题,程序无法启动。(其实在主bean加上@Lazy也能解决)

原因主要是主bean对象通过构造函数注入所依赖bean对象时,无法创建该所依赖的bean对象,获取该所依赖bean对象的引用。因为如下代码所示。

创建主bean对象,调用顺序为:

1.调用构造函数,2. 放到三级缓存,3. 属性赋值。其中调用构造函数时会触发所依赖的bean对象的创建。
    // bean对象实例创建的核心实现方法
    protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
    		throws BeanCreationException {
    		// 省略其他代码
    		// 1. 调用构造函数创建该bean对象,若不存在构造函数注入,顺利通过
    		instanceWrapper = createBeanInstance(beanName, mbd, args);
    		// 2. 在singletonFactories缓存中,放入该bean对象,以便解决循环依赖问题
    		addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    		// 3. populateBean方法:bean对象的属性赋值
    		populateBean(beanName, mbd, instanceWrapper); 		
    		// 省略其他代码
    	return exposedObject;
    }

createBeanInstance是调用构造函数创建主bean对象,在里面会注入构造函数中所依赖的bean,而此时并没有执行到addSingletonFactory方法来添加主bean对象的创建工厂到三级缓存singletonFactories中。故在createBeanInstance内部,注入和创建该主bean对象时,如果在构造函数中存在对其他bean对象的依赖,并且该bean对象也存在对主bean对象的依赖,则会出现循环依赖问题,原理如下:

主bean对象为AA对象依赖于B对象,B对象也存在对A对象的依赖,创建A对象时,会触发B对象的创建,则B无法通过
三级缓存机制获取主bean对象A的引用(即B如果通过构造函数注入A,则无法创建B对象;如果通过属性注入或者
setter方法注入A,则创建B对象后,对B对象进行属性赋值,会卡在populateBean方法也无法返回)。 故无法创建主
bean对象所依赖的B,创建主bean对象A时,createBeanInstance方法无法返回,出现代码死锁,程序报循环依赖错
误。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/69550.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MySQL】数据处理函数

只有当你开始相信自己时&#xff0c;你才拥有真正的人生。——《洛奇》 前言&#xff1a; 大家好&#xff0c;我是爱打拳的程序猿。今天给大家展示是数据处理函数的用法&#xff0c;分为单行处理函数和分组函数。数据处理函数主要是为了更方便解决数据的各种问题。文章以代码和…

2023年Unity UI教程

2023年Unity UI教程 Unity 新 UI 系统 UI 工具包的完整概述 课程英文名&#xff1a;Modern Unity UI with UI Toolkit 此视频教程共10.0小时&#xff0c;中英双语字幕&#xff0c;画质清晰无水印&#xff0c;源码附件全 下载地址 课程编号&#xff1a;336 百度网盘地址&am…

SSM框架-获取容器的方式

9 容器 9.1 获取容器 记载类路径下的容器所在的配置文件,也就是之前用的 public class App2 {public static void main(String[] args) {ApplicationContext ctx new ClassPathXmlApplicationContext("applicationContext.xml");BookDao bookDao (BookDao) ctx.…

liunx 部署 kkfileview文件预览 以及解决https访问

kkfileview gitee地址 https://gitee.com/kekingcn/file-online-preview 1.下载office组件 wget https://kkfileview.keking.cn/LibreOffice_7.1.4_Linux_x86-64_rpm.tar.gz tar -zxvf LibreOffice_7.1.4_Linux_x86-64_rpm.tar.gz cd LibreOffice_7.1.4.2_Linux_x86-64_rpm/R…

TensorRT推理手写数字分类(一)

系列文章目录 &#xff08;一&#xff09;使用pytorch搭建模型并训练 文章目录系列文章目录前言一、网络搭建1.LeNet网络结构2.pytorch代码二、网络训练1.pytorch代码2.结果展示三、保存和加载模型1.保存整个网络2.保存网络中的参数总结前言 为了学习一下使用TensorRT进行推理…

硬核!Github星标79.4K的阿里强推Java面试参考指南到底有多强?

谈到Java面试&#xff0c;相信大家第一时间脑子里想到的词肯定是金三银四&#xff0c;金九银十。好像大家的潜意识里做Java开发的都得在这个时候才能出去面试&#xff0c;跳槽成功率才高&#xff01;但LZ不这么认为&#xff0c;LZ觉得我们做技术的一生中会遇到很多大大小小的面…

Java并发编程—如何写好代码?链式调用该怎么玩?

目录 一、案例说明 二、原生方式代码流程 三、链式调用代码流程 四、链式调用手搓的方式&#xff1a; 五、总结&#xff1a; 在上一篇博客https://blog.csdn.net/qq_52545155/article/details/128212148?spm1001.2014.3001.5501&#xff0c;博主在写商城统计商品价格的时…

mybatis中其他数据源也使用XML进行操作(SqlSessionFactory.openSession(Connection connection)方法)

文章目录1. 前言2. 先说结论3. 例子1. 准备数据2. 思考过程3. 结论1. 前言 当前在使用springbootmybatis的时候&#xff0c;通常会先在配置文件中配置好数据源&#xff0c;并在Mapper.xml文件编写好相关SQL&#xff0c;使用mybatis进行对数据库进行所谓的crud操作。 有时候会出…

nginx代理https妈妈级手册

目录 背景说明 相关地址 https证书生成 nginx安装及配置 结果展示​编辑 背景说明 为了保证传输加密、访问安全&#xff0c;我们采用nginx服务器将http服务代理为https。所需材料&#xff1a;openssl&#xff08;用来生成证书&#xff09;、http服务、nginx自身。 相关地址…

C/C++第三方库zeromq、log4cpp交叉编译、本地安装ubuntu180.04

一、zeromq的编译安装 1&#xff09;ubuntu下命令 apt-get install libzmq3-dev不推荐这种方式&#xff0c;因为很可能安装的版本并不是最新的&#xff1b; 2&#xff09;自己编译安装&#xff08;推荐&#xff09; 地址&#xff1a;https://github.com/zeromq/libzmq/relea…

设计模式--装饰者模式

文章目录前言一、未使用设计模式二、装饰者模式1.定义2.角色三、应用场景四、优缺点优缺前言 晓子&#xff08;咖啡店员&#xff09;&#xff0c;来一杯美式&#xff0c;加点威士忌和砂糖。 抱歉啊&#xff0c;猫。收银系统还没有你说的组合&#xff0c;要不换一个&#x1f60…

React 的调度系统 Scheduler

大家好&#xff0c;我是前端西瓜哥。今天来学习 React 的调度系统 Scheduler。 React 版本为 18.2.0 React 使用了全新的 Fiber 架构&#xff0c;将原本需要一次性递归找出所有的改变&#xff0c;并一次性更新真实 DOM 的流程&#xff0c;改成通过时间分片&#xff0c;先分成一…

nnUnet测试

https://github.com/MIC-DKFZ/nnUNet nnUnet要在Windows上跑起来有点麻烦&#xff0c;主要是项目路径的问题&#xff0c;我目前测试了2分类遥感数据&#xff08;其实只要是二分类都行&#xff0c;无所谓什么数据&#xff09;&#xff0c;我这里说难是因为我没有安装&#xff0…

【SQL】MVCC 多版本并发控制

MVCC多版本并发控制快照读与当前读隔离级别隐藏字段&#xff0c;undo log 版本链隐藏字段trx_id版本链read view举例说明read committed&#xff08;读已提交&#xff09;隔离级别下repeatable read&#xff08;可重复读&#xff09;隔离级别下innodb如何解决幻读总结并发问题的…

LaTex使用技巧9:argmin / argmax下标写法

记录两种写法 1.arg⁡max⁡θ\mathop{\arg\max}\limits_{\theta}θargmax​的写法 写法1&#xff1a; $\mathop{\arg\max}\limits_{\theta}$ 写法2&#xff1a; $\sideset{}{}{\arg\max}_{\theta}^{} $ 2.arg⁡min⁡θ\mathop{\arg\min}\limits_{\theta}θargmin​的写法 写法…

STL常用生成算法和集合算法(20221207)

STL的常用算法 概述&#xff1a; 算法主要是由头文件<algorithm> <functional> <numeric> 组成。 <algorithm>是所有STL头文件中最大的一个&#xff0c;涉及比较、交换、查找、遍历等等&#xff1b; <functional>定义了一些模板类&#xff0…

做一个公司网站大概要多少钱?

做一个公司网站大概要多少钱&#xff0c;很多公司在做网站之前可能已经简单了解过费用&#xff0c;但是费用差距都会比较大&#xff0c;为什么的呢&#xff0c;因为一般都是受到制作方式因素的影响。下面给大家说说不同的方式做一个公司网站大概要多少钱。 一、自己/团队做公司…

SQLyog —— 图形化工具使用

SQLyog下载链接&#xff1a; 点击跳转 在这一篇内容MySQL数据库 —— 常用语句当中讲到关于MySQL数据库命令的基本使用&#xff0c;这一篇是关于SQLyog数据库图形化工具的内容&#xff0c;先进行安装演示后在通过SQLyog进行操作数据库&#xff1a; SQLyog 安装 下载完成之后双击…

pageoffice在线打开word文件加盖电子印章

一、加盖印章的 js 方法 js方法 二、常见使用场景 1、常规盖章。弹出用户名、密码输入框&#xff0c;选择对应印章。 点击盖章按钮弹出用户名密码登录框&#xff0c;登录以后显示选择电子印章。 document.getElementById("PageOfficeCtrl1").ZoomSeal.AddSeal(…

Python模块pathlib操作文件和目录操作总结

前言 目前大家常用的对于文件和操作的操作使用 os.path 较多&#xff0c;比如 获取当前路径os.getcwd()&#xff0c;判断文件路径是否存在os.path.exists(folder) 等等。 在Python3.4开始&#xff0c;官方提供了 pathlib 面向对象的文件系统路径&#xff0c;核心的点在于 面向…