Redian新闻
>
从源码层面深度剖析 Spring 循环依赖

从源码层面深度剖析 Spring 循环依赖

公众号新闻

来源 | OSCHINA 社区

作者 | 京东云开发者-郭艳红

原文链接:https://my.oschina.net/u/4090830/blog/5612702

以下举例皆针对单例模式讨论
图解参考:https://www.processon.com/view/link/60e3b0ae0e3e74200e2478ce

1、Spring 如何创建 Bean?

对于单例 Bean 来说,在 Spring 容器整个生命周期内,有且只有一个对象。
Spring 在创建 Bean 过程中,使用到了三级缓存,即 DefaultSingletonBeanRegistry.java 中定义的:
    /** 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);

以 com.gyh.general 包下的 OneBean 为例,debug springboot 启动过程,分析 spring 是如何创建 bean 的。

参考图中 spring 创建 bean 的过程。其中最关键的几步有:
1. getSingleton(beanName, true) 依次从一二三级缓存中查找 bean 对象,如果缓存中存在对象,则直接返回 (early);
2. createBeanInstance(beanName, mbd, args) 选一个合适的构造函数,new 实例对象 (instance),此时的 instance 中依赖的属性还都是 null,属于半成品;
3. singletonFactories.put(beanName, oneSingletonFactory) 利用上一步的 instance,构建一个 singletonFactory,并将其放到三级缓存中;
4. populateBean(beanName, mbd, instanceWrapper) 填充 bean:为该 bean 定义的属性创建对象或赋值;
5. initializeBean("one",oneInstance, mbd) 初始化 bean:对 bean 进行初始化或其他加工,如生成代理对象 (proxy);
6. getSingleton(beanName, false) 依次在一二级缓存中查找,检查是否有因循环依赖导致提前生成的对象,有的话与初始化后的对象是否一致;

2、Spring 如何解决循环依赖?

以 com.gyh.circular.threeCache 包下的 OneBean 和 TwoBean 为例 ,两个 Bean 相互依赖(即形成闭环)。
参考图中 spring 解决循环依赖 的过程可知,spring 利用三级缓中的 objectFactory 生成并返回一个 early 对象,提前暴露这个 early 地址,供其他对象依赖注入使用,以此解决循环依赖问题。

3、Spring 不能解决哪些循环依赖?

3.1 循环中使用了 @Async 注解

3.1.1 为什么循环中使用了 @Async 会报错?

以 com.gyh.circular.async 包下的 OneBean 和 TwoBean 为例,两个 bean 相互依赖,且 oneBean 中的方法使用了 @Async 注解,此时启动 spring 失败,报错信息为:org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a.one': Bean with name 'a.one' has been injected into other beans [a.two] 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.
并通过 debug 代码,发现报错位置在 AbstractAutowireCapableBeanFactory#doCreateBean 方法内,由于 earlySingletonReference != null 且 exposedObject != bean,导致报错。
 
结合流程图中 spring 解决循环依赖 及上述图片中可知:
1. 行 1 中 bean 为 createBeanInstance 创建的实例 (address1)
2. 行 2 中 exposedObject 为 initializeBean 后生成的代理对象 (address2)
3. 行 3 中 earlySingletonReference 为 getEarlyBeanReference 时创建的对象【此处地址同 bean (address1)】
深层原因为:先前 TwoBean 在 populateBean 时已经依赖了地址为 address1 的 earlySingletonReference 对象,而此时 OneBean 经过 initializeBean 之后,返回了地址为 address2 的新对象,导致 spring 不知道哪个才是最终版的 bean,所以报错。
earlySingletonReference 是如何生成的,参考 getSingleton ("one", true) 过程。

3.1.2 循环中使用了 @Async 一定会报错吗?

依然以 com.gyh.circular.async 包下的 OneBean 和 TwoBean 为例,两个 bean 相互依赖,使 TwoBean (非 OneBean) 中的方法使用了 @Async 注解,此时启动 spring 成功,并未报错。
debug 代码可知:虽然 TwoBean 使用了 @Async 注解,但其 earlySingletonReference = null; 故不会引起报错。
深层原因为:OneBean 先被创建,TwoBean 后创建,再整条链路中,并未在三级缓存中查找过 TwoBean 的 objectFactory 。(OneBean 在创建过程中,被找过两次,即 one-> two ->one;TwoBean 的创建过程中,只找过它一次,即 two ->one。)
由此可得:@Async 造成循环依赖报错的先约条件为:
1. 循环依赖中的 Bean 使用了 @Async 注解
2. 且这个 Bean,比循环内其他 Bean 先创建。
3. 注:一个 Bean 可能会同时存在于多个循环内;只要存在它是某个循环内第一个被创建的 Bean,那么就会报错。

3.1.3 为什么循环中使用了 @Transactional 不会报错?

已知使用了 @Transactional 注解的 Bean,Spring 也会为其生成代理对象,但为什么这种 Bean 在循环里时不会产生报错呢?
以 com.gyh.circular.transactional 包下的 OneBean 和 TwoBean 为例,两个 Bean 相互依赖,且 OneBean 中的方法使用了 @Transactional 注解,启动 Spring 成功,并不会报错。
debug 代码可知,生成 OneBean 过程中,虽然 earlySingletonReference != null,但 initializeBean 之后的 exposedObject 和 原始实例的地址相同(即 initializeBean 步骤中,并未对实例生成代理),所以不会产生报错。
3.1.4 为什么同样是代理会产生两种不同的现象?
同样是生成代理对象,同样是参与到循环依赖中,会产生不同现象的原因是:当他们处在循环依赖中时,生成代理的节点不同:
1. @Transactional 在 getEarlyBeanReference 时生成代理,提前暴露出代理之后的地址(即最终地址);
2. @Async 在 initializeBean 时生成代理,导致提前暴露出去的地址不是最终地址,造成报错。
为什么 @Async 不能在 getEarlyBeanReference 时生成代理呢?对比下两者执行的代码过程发现:
两者都是在 AbstractAutoProxyCreator#getEarlyBeanReference 的方法对原始实例对象进行包装,如下图
 使用 @Transactional 的 Bean 在 create proxy 时,获取到一个 advice ,随即生成了代理对象 proxy.
而使用 @Async 的 Bean 在 create proxy 时,没有获取到 advice,不能被代理.

3.1.5 为什么 @Async 在 getEarlyBeanReference 时不能返回一个 advice?

在 AbstractAutoProxyCreator#getAdvicesAndAdvisorsForBean 方法内,其主要做的事情有:
1. 找到当前 spring 容器中所有的 Advisor
2. 返回适配当前 bean 的所有 Advisor
第一步返回的 Advisor 有 BeanFactoryCacheOperationSourceAdvisor 和 BeanFactoryTransactionAttributeSourceAdvisor,并无处理 Async 相关的 Advisor.
刨根问底,追查为什么第一步不会返回处理 Async 相关的 Advisor?
已知使用 @Async @Transactional @Cacheable 需要提前进行开启,即提前标注 @EnableAsync、@EnableTransactionManagement、@EnableCaching 。
以 @EnableTransactionManagement、@EnableCaching 为例,在其注解定义中,引入了 Selector 类,Selector 中又引入了 Configuration 类,在 Configuration 类中,创建了对应 Advisor 并放到了 spring 容器中,所以第一步才能得到这两个 Advisor.
而 @EnableAsync 的定义中引入的 Configuration 类,创建的是 AsyncAnnotationBeanPostProcessor 并非一个 Advisor,所以第一步不会得到它,所以 @Async 的 bean 不会在这一步被代理。

3.2 构造函数引起的循环依赖

以 com.gyh.circular.constructor 包下的 OneBean 和 TwoBean 为例,两个类的构造函数中各自依赖对方,启动 spring,报错:org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'c.one': Requested bean is currently in creation: Is there an unresolvable circular reference?
debug 代码可知,两个 bean 在根据构造函数 new instance 时,就已经陷入的死循环,无法提前暴露可用的地址,所以只能报错。

4、如何解决以上循环依赖报错?

1. 不用 @Async,将需要异步操作的方法,放到线程池中执行。(推荐)
2. 提出 @Async 标注的方法。(推荐)
3. 将使用 @Async 的方法提出到单独的类中,该类只做异步处理,不做其他业务依赖,即避免形成循环依赖,从而解决报错问题。参考 com.gyh.circular.async.extract 包。
4. 尽量不使用构造函数依赖对象。(推荐)
5. 破坏循环(不推荐)即不形成闭环,在开发之前,规划好对象依赖,方法调用链,尽量做到不使用循环依赖。(较难,随着迭代开发不断变化,很可能产生循环)
6. 破坏创建顺序(不推荐)
7. 由于使用 @Async 注解的所在类,比循环依赖内其他类先创建时才会报错,那么想办法使该类不先于其他类先创建,也可解决该问题,如:@DependsOn、  @Lazy

END



开源白嫖不提倡?



这里有最新开源资讯、软件更新、技术干货等内容

点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦~

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
微服务循环依赖调用引发的血案狂飙《孙子兵法》顶级谋略,每个人都需要学习!(深度剖析)默克详解显示行业可持续发展:从材料抓起、从源头做起新冠翻篇记录Blender自学基础教程2.0——5.物体体块层面的造型规律-2美国入境档案--刘瑞恒。另2人的联系人李大钊全力打响药品保供“攻坚战” ,从源头上缓解广大群众“买药难”从源头禁烟,这些人的子孙后代都不能购买和抽!九大投行|Credit Suisse Securities Research Spring Program正在进行中!如何修改 Nginx 源码实现 worker 进程隔离长文深度剖析Biotech产业:在有限的知识和经验的迷雾中穿行实习快讯|德意志银行开放2023 Spring Into Banking计划2023哪些热点最香?CNS级大佬带你深度剖析6大热点的前世今生!美股SPAC|创新排放监测提供商 Spectaire Inc. 将通过与 SPAC合并上市实用英语情景对话|English speaking practiceSpringBoot+Prometheus+Grafana 实现自定义监控工程与产品的胜利,深度剖析ChatGPT和聪明地设计基础架构相比于依赖别人,天秤座更容易被人依赖对话 Spring 大神:Spring 生态系统的新时代来了!趣图:部署软件时误用了循环依赖共和百载复清零我烤的coho 三文鱼 太好吃啦深入剖析 Spring Boot 的 SPI 机制今日实习|德意志银行开放2023 Spring Into Banking计划调研报告:深度剖析高校教师的处境及出路Spring for Apache Kafka 3.0 和 Spring for RabbitMQ 3.0 发布Hadoop/Spark 太重,esProc SPL 很轻美股SPAC|中国SPAC Horizo​​n Space Acquisition I申请6000万美元IPO,目标新兴成长公司天赋“易昺(bǐng)”,创造历史!臧律师深度剖析:美国EB-1A杰出人才移民深度剖析热点事件获10万转发,视频号近期的创作风向是这些?源码级深度理解Java SPI胡锦涛中途被“请出”会场SpringBoot + Prometheus + Grafana 打造可视化监控一条龙!Blender自学基础教程2.0——5.物体体块层面的造型规律-1
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。