浅析Spring中的循环依赖问题

 更新时间:2023年11月14日 08:52:51   作者:荆轲刺秦  
这篇文章主要介绍了浅析Spring中的循环依赖问题,Spring 是利用了 三级缓存 来解决循环依赖的,其实现本质是通过提前暴露已经实例化但尚未初始化的 bean 来完成的,需要的朋友可以参考下

Spring的循环依赖

本文不会详细讲解 Spring 循环依赖的基础问题。

我相信能阅读到本文的,对 Spring 循环依赖已经有一定了解,但可能存在一些疑惑。本文就是尝试来解决这些疑惑的。

我们都知道 Spring 是利用了 三级缓存 来解决循环依赖的,其实现本质是通过提前暴露已经实例化但尚未初始化的 bean 来完成的。

但是呢,我们仍然会想,这里为什么要使用三级缓存?而且,我相信,不少人都曾手写过代码来解决循环依赖的问题,那时候,他们也只用了二级缓存,参考下图:

循环依赖二级缓存

我们可以仔细跟踪序号,理清整个流程。所以,二级缓存是能够解决循环依赖,这也符合它的本质:“提前暴露对象”。这个流程图并没有描述接下来的流程,这里使用文字简单描述下:

  • 对象A获取到已创建完成的对象B注入;
  • 对象A完成字段注入以及初始化,并放入一级缓存;
  • 对象A从二级缓存中移除;

既然二级缓存能够解决循环依赖了,那为什么要使用三级缓存呢?网上的说法是,那是因为 Spring 中存在替换注入对象的问题。通俗地来说就是:“一个半成品对象有可能在被对象b注入以后,被更改为其它的实例对象,那么对象b注入的就是一个过期的对象了”。

这种情况会导致对象b注入了一个并不存在于容器中的对象A(因为被更改后的对象注入了容器,替换掉了原来的对象)。所以,大多数人会认为三级缓存是为了解决这个问题的,让我们来看看真的是如此嘛?上代码:

@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

}
@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA;

}
@Component
public class ResetServiceABeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if(beanName.equals("serviceA")){
            return new ServiceA();
        }
        return bean;
    }
}

ServiceA 和 ServiceB 互相依赖,ResetServiceABeanPostProcessor 则是为了在ServiceB 注入了原来的ServiceA 后,将原来的 ServiceA 给替换掉。我们模拟了上述场景,但最后的运行结果却是得到了一个 BeanCurrentlyInCreationException 异常,异常在图中的 620 行抛出。

异常抛出点

可以发现,似乎它并没有解决这个“注入了过期对象”的问题,可是它至少检测出了这个问题。所以,我个人认为,三级缓存并不是来解决这个问题,而是来在启动时检测这个问题的。

文章写到这里似乎也差不多了,但我还想纠正一点,网上有很多对于三级缓存的描述如下:

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
	...
	// 从上至下 分表代表这“三级缓存”
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); //一级缓存
	private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二级缓存
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级缓存
	...
	
	/** Names of beans that are currently in creation. */
	// 这个缓存也十分重要:它表示bean创建过程中都会在里面呆着~
	// 它在Bean开始创建时放值,创建完成时会将其移出~
	private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

	/** Names of beans that have already been created at least once. */
	// 当这个Bean被创建完成后,会标记为这个 注意:这里是set集合 不会重复
	// 至少被创建了一次的  都会放进这里~~~~
	private final Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256));
}

但源码中的顺序却不是如此(我使用的是 5.1.5 版本):

	/** 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 HashMap<>(16);

但我想三级缓存的分层并不是依赖上面的源码顺序来分为一二三的,而应该是根据从缓存中获取对象的顺序来分层的:

	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
				singletonObject = this.earlySingletonObjects.get(beanName);
				if (singletonObject == null && allowEarlyReference) {
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}

最后,我结合上述的场景来分析下这三个“缓存”中元素的变化(省略了其它无关流程):

缓存变化

红色线条代表取出元素,虚线代表最终将不存在。

这里做下总结:

二级缓存也是能解决循环依赖的,使用三级缓存是为了帮助检测提前暴露的对象在后期被修改的这种情况;

通过 earlySingletonObjects 持有被暴露的对象,然后在最终返回对象时进行比对。如果不是同一个对象,则代表发生了对象后期被修改的情况。

到此这篇关于浅析Spring中的循环依赖问题的文章就介绍到这了,更多相关Spring的循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Springboot拦截器如何获取@RequestBody参数

    Springboot拦截器如何获取@RequestBody参数

    这篇文章主要介绍了Springboot拦截器如何获取@RequestBody参数的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • Java花式解决'分割回文串 ii'问题详解

    Java花式解决'分割回文串 ii'问题详解

    最学习动态规划思想的路上,遇见了‘分割回文串问题’,如临大敌啊,题目听起来蛮简单,思考起来却也没那么容易,本文将为大家详细介绍几种解决分割回文串 ii问题的办法,需要的可以参考一下
    2021-12-12
  • Java中解压缩文件的方法详解(通用)

    Java中解压缩文件的方法详解(通用)

    在软件开发和数据处理领域,文件的解压缩和压缩是常见的任务,下面这篇文章主要给大家介绍了关于Java中解压缩文件的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-06-06
  • 详谈Java中的事件监听机制

    详谈Java中的事件监听机制

    下面小编就为大家带来一篇详谈Java中的事件监听机制。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • Java实现将PDF转为图片格式的方法详解

    Java实现将PDF转为图片格式的方法详解

    PDF文件和图片文件,这是两种完全不一样的格式,可是有的时候这两种格式却是有相互转换的需要,本文将介绍如何通过Java应用程序快速高效地将PDF转为图片格式。一起来看看吧
    2023-03-03
  • Java 创建两个线程模拟对话并交替输出实现解析

    Java 创建两个线程模拟对话并交替输出实现解析

    这篇文章主要介绍了Java 创建两个线程模拟对话并交替输出实现解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-10-10
  • 浅谈BeanPostProcessor加载次序及其对Bean造成的影响分析

    浅谈BeanPostProcessor加载次序及其对Bean造成的影响分析

    这篇文章主要介绍了浅谈BeanPostProcessor加载次序及其对Bean造成的影响分析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-04-04
  • JVM 方法调用之动态分派(详解)

    JVM 方法调用之动态分派(详解)

    下面小编就为大家带来一篇JVM 方法调用之动态分派(详解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • SpringBoot整合Drools的实现步骤

    SpringBoot整合Drools的实现步骤

    Drools是一个易于访问企业策略、易于调整以及易于管理的开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行所需的业务规则。本文将讲述SpringBoot整合Drools的步骤
    2021-05-05
  • IntelliJ IDEA 的 Spring 项目如何查看 @Value 的配置和值(方法详解)

    IntelliJ IDEA 的 Spring 项目如何查看 @Value 的配置和值(方法详解)

    这篇文章主要介绍了IntelliJ IDEA 的 Spring 项目如何查看 @Value 的配置和值,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-10-10

最新评论