浅析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的循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • idea中@override报错在实现接口方法时无法使用的解决

    idea中@override报错在实现接口方法时无法使用的解决

    文章介绍了在使用IDEA新建Filter类时遇到的@Override注解报错问题,原因是JDK版本过低,通过在maven配置文件中设置编译插件或更改项目JDK版本,解决了该问题
    2025-12-12
  • 建议你使用LocalDateTime而不是Date哦

    建议你使用LocalDateTime而不是Date哦

    这篇文章主要介绍了建议你使用LocalDateTime而不是Date,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • Spring AI Alibaba框架构建智能体Agent的完整指南

    Spring AI Alibaba框架构建智能体Agent的完整指南

    随着大语言模型(LLM)技术的快速发展,构建智能Agent应用变得越来越简单,本文将通过两个实际的代码示例,展示如何使用Spring AI Alibaba框架构建功能丰富的天气查询Agent,感兴趣的小伙伴可以了解下
    2026-03-03
  • Java实现的Excel列号数字与字母互相转换功能

    Java实现的Excel列号数字与字母互相转换功能

    这篇文章主要介绍了Java实现的Excel列号数字与字母互相转换功能,涉及java针对Excel相关数值与字符串操作技巧,需要的朋友可以参考下
    2018-03-03
  • Java深入了解数据结构中常见的排序算法

    Java深入了解数据结构中常见的排序算法

    这篇文章主要介绍了Java常用的排序算法及代码实现,在Java开发中,对排序的应用需要熟练的掌握,这样才能够确保Java学习时候能够有扎实的基础能力。那Java有哪些排序算法呢?本文小编就来详细说说Java常见的排序算法,需要的朋友可以参考一下
    2022-01-01
  • Java 中间件Dubbo 服务降级以及Mock 机制详解

    Java 中间件Dubbo 服务降级以及Mock 机制详解

    本文将深入探讨Dubbo的Mock机制,从原理、配置方式、使用场景到实战案例,全面解析如何利用这一特性构建高可用的微服务系统,感兴趣的朋友跟随小编一起看看吧
    2026-02-02
  • springboot中使用自定义两级缓存的方法

    springboot中使用自定义两级缓存的方法

    这篇文章主要介绍了springboot中使用自定义两级缓存的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-05-05
  • Spring源码分析容器启动流程

    Spring源码分析容器启动流程

    Spring的启动流程可以归纳为三个步骤:初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中、将配置类的BeanDefinition注册到容器中、调用refresh()方法刷新容器
    2022-09-09
  • redisson特性及优雅实现示例

    redisson特性及优雅实现示例

    这篇文章主要为大家介绍了redisson特性及优雅实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • 2021年最新Redis面试题汇总(1)

    2021年最新Redis面试题汇总(1)

    在程序员面试过程中redis相关的知识是常被问到的话题。这篇文章主要介绍了几道Redis面试题,整理一下分享给大家,感兴趣的小伙伴们可以参考一下
    2021-07-07

最新评论