系统学习SpringFramework:循环依赖与三级缓存

本篇内容包括:Spring 中的循环依赖问题(包括 Spring 中的循环依赖问题和Spring 中的循环依赖的 5 种场景的介绍)、Spring 三级缓存介绍、4 个 Spring 无法自动解决的循环以来场景以及其对应的手动解决方式。

一、Spring 中的循环依赖问题

1、Spring 中的循环依赖概述

Spring 循环依赖指的是 SpringBean 对象之间的依赖关系形成一个闭环。即在代码中,把两个或者多个 Bean 相互之间去持有对方的引用,就会发生循环依赖,循环依赖会导致注入出现死循环,这是 Spring 发生循环依赖的主要原因之一。

Spring 循环依赖主要有三种情况,即:自身依赖自身,两者互相依赖,多者循环依赖

  1. 自身依赖自身:自己依赖自己的直接依赖
  2. 两者互相依赖:两个对象之间的直接依赖
  3. 多者循环依赖:多个对象之间的间接依赖

Spring循环依赖问题

自身依赖自身,两者互相依赖 两者互相依赖的情况比较直观,很好辨识,但是我们工作中最有可能触发的还是多者循环依赖,多者循环依赖的情况有时候因为业务代码调用层级很深,不容易识别出来。但无论循环依赖的数量有多少,循环依赖的本质是一样的。就是你的完整创建依赖于我,而我的完整创建也依赖于你,但我们互相没法解耦,最终导致依赖创建失败。

2、Spring 中的循环依赖的 5 种场景

Spring 中出现循环依赖主要有着 5 种场景: ①、单例的 setter 注入(能解决);②、多例的 setter 注入(不能解决);③、构造器注入(不能解决);④、单例的代理对象 setter 注入(有可能解决);⑤、DependsOn 循环依赖(不能解决)。接下来我们逐一来看。

img

二、Spring 三级缓存

1、spring 创建 bean 的流程

在开始理解 Spring 三级缓存如何让解决循环依赖问题前我们先来温习一下 spring 创建 bean 的流程:

  1. Spring 启动时会根据配置文件或启动类把所有的 bean 注册成 bean 定义(就是映射 <bean> 标签属性的 Java 类)

  2. 遍历 bean 定义中的 beanName,调用 BeanFactory#getBean(beanName) 方法创建、初始化并返回 bean 实例

    其中 getBean 方法:

    1. 先从缓存(一层到三层依次获取)拿,没有就去创建;
    2. 创建 Bean 时,把 beanName 标记为正在创建中,通过其定义里的 class 找到构造器方法反射创建实例,并把其对象工厂放入第三层缓存
    3. 对实例初始化,移除正在创建中的标记,把实例放入第一层缓存,移除第二、三层中的缓存,最后返回实例

Ps1:实例初始化过程:获取此 bean 中有 @Autowired 等注解的成员变量,从所有 bean 定义中找出此类型的 beanName,又通过 BeanFactory#getBean 方法获取实例,然后反射设值成员变量。

Ps2:上述流程中 斜体 部分为触发循环依赖时多出主流程的步骤。

位于 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry 中的三级缓存源码:

	/** Cache of singleton objects: bean name to bean instance. */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/** Cache of singleton factories: bean name to ObjectFactory. */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	/** Cache of early singleton objects: bean name to bean instance. */
	private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
2、场景一:单例的 setter 注入

这种注入方式应该是 Spring 中最常见的,Demo 如下:

@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    public void test1() {
    }
}

@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }
}

在上述代码中,就是一个经典的循环依赖,其中 TestService1 依赖 TestService2,TestService2 依赖 TestService1 构成了一个简单了两者互相依赖关系,但是我们在使用类似代码时,并没有感知过该类型的循环依赖存在,因为此种类型已经被 Spring 默默解决了。

3、三级缓存

Spring 内部有三级缓存:

  • 一级缓存(singletonObjects),用于保存实例化、注入、初始化完成的 Bean 实例
  • 二级缓存(earlySingletonObjects),用于保存实例化完成的 Bean 实例
  • 三级缓存(singletonFactories),用于保存 Bean 的创建工厂,以便于后面扩展有机会创建代理对象。

以上面 Demo 为例,现在项目启动,spring 开始创建 bean,比如先创建 TestService1:

  1. 标记 TestService1 为正在创建中,反射创建其实例,其对象工厂放入第三层缓存
  2. 初始化 TestService1 实例化时发现需要依赖注入 TestService2,则获取 TestService2 的实例
  3. 标记 TestService2 为正在创建中,反射创建其实例,其对象工厂放入第三层缓存
  4. 初始化 TestService2 实例化时发现需要依赖注入 TestService1,则获取 TestService1 的实例
  5. 这时候从缓存中获取时,TestService1 为正在创建中且第三层缓存有 TestService1 的值了,所以调用缓存的对象工厂的 getObject 方法,把返回的 TestService1 实例放入第二层缓存,删除第三层缓存
  6. TestService2 实例初始化完成,放入第一层缓存,移除第二、三层中的缓存
  7. 回到第 2 步,TestService1 实例初始化完成,放入第一层缓存,移除第二、三层中的缓存

img

下面是 getBean(beanName) 方法最先调用的从这三层缓存中获取 bean 实例的逻辑(即上面第5步)

	/**
	 * Return the (raw) singleton object registered under the given name.
	 * <p>Checks already instantiated singletons and also allows for an early
	 * reference to a currently created singleton (resolving a circular reference).
	 * @param beanName the name of the bean to look for
	 * @param allowEarlyReference whether early references should be created or not
	 * @return the registered singleton object, or {@code null} if none found
	 */
	@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;
	}

以及一直提到的对象工厂,及其 getObject 方法的实现:

	/**
	 * Obtain a reference for early access to the specified bean,
	 * typically for the purpose of resolving a circular reference.
	 * @param beanName the name of the bean (for error handling purposes)
	 * @param mbd the merged bean definition for the bean
	 * @param bean the raw bean instance
	 * @return the object to expose as bean reference
	 */
	protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
		Object exposedObject = bean;
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
			for (BeanPostProcessor bp : getBeanPostProcessors()) {
				if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
					SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
				}
			}
		}
		return exposedObject;
	}
4、关于二级缓存

细心的朋友可能会发现在这种场景中第二级缓存作用不大。那么问题来了,为什么要用第二级缓存呢?

试想一下,如果出现以下这种情况,我们要如何处理?

@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    @Autowired
    private TestService3 testService3;
    public void test1() {
    }
}
@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }
}
@Service
public class TestService3 {
    @Autowired
    private TestService1 testService1;
    public void test3() {
    }
}

TestService1 依赖于 TestService2 和 TestService3,而 TestService2 依赖于 TestService1,同时 TestService3 也依赖于 TestService1。按照上图的流程可以把 TestService1 注入到 TestService2,并且 TestService1 的实例是从第三级缓存中获取的。

假设不用第二级缓存,TestService1 注入到 TestService3 的流程如图:

img

TestService1 注入到 TestService3 又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是 ObjectFactory对象。说白了,两次从三级缓存中获取都是 ObjectFactory 对象,而通过它创建的实例对象每次可能都不一样的。这样不是有问题?

为了解决这个问题,Spring 引入的第二级缓存。上面其实 TestService1 对象的实例已经被添加到第二级缓存中了,而在 TestService1 注入到 TestService3 时,只用从第二级缓存中获取该对象即可。

img

还有个问题,第三级缓存中为什么要添加 ObjectFactory 对象,直接保存实例对象不行吗?答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。


三、循环依赖的其他 4 种场景

1、多例的 setter 注入

这种注入方法偶然会有,特别是在多线程的场景下,具体代码如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    public void test1() {
    }
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }
}

在上述多例的 setter 注入情况下,Spring 程序也是能够正常启动启动的,其实在 AbstractApplicationContext 类的 refresh方法中告诉了我们答案,它会调用 finishBeanFactoryInitialization 方法,该方法的作用是为了 Spring 容器启动的时候提前初始化一些 Bean。该方法的内部又调用了 preInstantiateSingletons 方法

@Override
	public void preInstantiateSingletons() throws BeansException {
		if (logger.isTraceEnabled()) {
			logger.trace("Pre-instantiating singletons in " + this);
		}

		// Iterate over a copy to allow for init methods which in turn register new bean definitions.
		// While this may not be part of the regular factory bootstrap, it does otherwise work fine.
		List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

		// Trigger initialization of all non-lazy singleton beans...
		for (String beanName : beanNames) {
			RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
			if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
				if (isFactoryBean(beanName)) {
					Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
					if (bean instanceof FactoryBean) {
						FactoryBean<?> factory = (FactoryBean<?>) bean;
						boolean isEagerInit;
						if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
							isEagerInit = AccessController.doPrivileged(
									(PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit,
									getAccessControlContext());
						}
						else {
							isEagerInit = (factory instanceof SmartFactoryBean &&
									((SmartFactoryBean<?>) factory).isEagerInit());
						}
						if (isEagerInit) {
							getBean(beanName);
						}
					}
				}
				else {
					getBean(beanName);
				}
			}
		}

其中 非抽象、单例 并且非懒加载的类才能被提前初始 Bean。,而多例即 SCOPE_PROTOTYPE 类型的类,非单例,不会被提前初始化 Bean,所以程序能够正常启动。如何让他提前初始化bean呢?

只需要再在 DEMO 中定义一个单例的类,在它里面注入 TestService1

@Service
public class TestService3 {
    @Autowired
    private TestService1 testService1;
}

重新启动程序,执行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出现了循环依赖。

Ps:这种循环依赖问题是无法解决的,因为它没有用缓存,每次都会生成一个新对象。

2、构造器注入

这种注入方式现在其实用的已经非常少了,但是我们还是有必要了解一下,如下代码:

@Service
public class TestService1 {
    public TestService1(TestService2 testService2) {
    }
}
@Service
public class TestService2 {
    public TestService2(TestService1 testService1) {
    }
}

运行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出现了循环依赖,为什么呢?

img

从图中的流程看出构造器注入没能添加到三级缓存,也没有使用缓存,所以也无法解决循环依赖问题。

3、单例的代理对象 setter 注入

这种注入方式其实也比较常用,比如平时使用:@Async 注解的场景,会通过 AOP 自动生成代理对象。

@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    @Async
    public void test1() {
    }
}
@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }
}

从前面得知程序启动会报错,出现了循环依赖,为什么会循环依赖呢?答案就在下面这张图中:

img

说白了,Bean 初始化完成之后,后面还有一步去检查:第二级缓存和原始对象是否相等。由于它对前面流程来说无关紧要,所以前面的流程图中省略了,但是在这里是关键点,我们重点说说:

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

正好是走到这段代码,发现第二级缓存和原始对象不相等,所以抛出了循环依赖的异常。如果这时候把 TestService1 改个名字,改成:TestService6,其他的都不变。

@Service
public class TestService6 {
    @Autowired
    private TestService2 testService2;
    @Async
    public void test1() {
    }
}

再重新启动一下程序,神奇般的好了。这是为什么?这就要从 Spring Bean 加载顺序说起了,默认情况下,Spring 是按照文件完整路径递归查找的,按路径+文件名排序,排在前面的先加载。所以 TestService1 比T estService2 先加载,而改了文件名称之后,TestService2 比 TestService6 先加载。

为什么 TestService2 比 TestService6 先加载就没问题呢?答案在下面这张图中:

img

这种情况 testService6 中其实第二级缓存是空的,不需要跟原始对象判断,所以不会抛出循环依赖。

4、DependsOn 循环依赖

还有一种有些特殊的场景,比如我们需要在实例化 Bean A 之前,先实例化 Bean B,这个时候就可以使用 @DependsOn 注解。

@DependsOn(value = "testService2")
@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    public void test1() {
    }
}
@DependsOn(value = "testService1")
@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }
}

程序启动之后,执行结果:

Circular depends-on relationship between 'testService2' and 'testService1'

这个例子中本来如果 TestService1 和 TestService2 都没有加 @DependsOn 注解是没问题的,反而加了这个注解会出现循环依赖问题。

这又是为什么?答案在 AbstractBeanFactory 类的 doGetBean 方法的这段代码中:

// Guarantee initialization of beans that the current bean depends on.
				String[] dependsOn = mbd.getDependsOn();
				if (dependsOn != null) {
					for (String dep : dependsOn) {
						if (isDependent(beanName, dep)) {
							throw new BeanCreationException(mbd.getResourceDescription(), beanName,
									"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
						}
						registerDependentBean(dep, beanName);
						try {
							getBean(dep);
						}
						catch (NoSuchBeanDefinitionException ex) {
							throw new BeanCreationException(mbd.getResourceDescription(), beanName,
									"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
						}
					}
				}

它会检查 dependsOn 的实例有没有循环依赖,如果有循环依赖则抛异常。


三、出现循环依赖如何解决?

项目中如果出现循环依赖问题,说明是 Spring 默认无法解决的循环依赖,要看项目的打印日志,属于哪种循环依赖。目前包含下面几种情况:

img

解决方式:

问题解决方式
生成代理对象产生的循环依赖①、 使用 @Lazy 注解,延迟加载 ②、使用 @DependsOn 注解,指定加载先后关系 ③、修改文件名称,改变循环依赖类的加载顺序
多例循环依赖可以通过把 Bean 改成单例的解决
构造器循环依赖可以通过使用 @Lazy 注解解决
使用 @DependsOn 产生的循环依赖要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栗筝i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值