循环依赖源码深度解析

 

singletonObjects (一级缓存)它是我们最熟悉的朋友,俗称“单例池”“容器”,缓存创建完成单例Bean的地方。
earlySingletonObjects(二级缓存)映射Bean的早期引用,也就是说在这个Map里的Bean不是完整的,甚至还不能称之为“Bean”,只是一个Instance.
singletonFactories(三级缓存) 映射创建Bean的原始工厂
 

这样做有什么好处呢?让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况:

spring初始化bean的过程如下:
1>首先尝试从一级缓存中获取serviceA实例,发现不存在并且serviceA不在创建过程中;
2>serviceA完成了初始化的第一步(实例化:调用createBeanInstance方法,即调用默认构造方法实例化);
3>将自己(serviceA)提前曝光到singletonFactories中;
4>此时进行初始化的第二步(注入属性serviceB),发现自己依赖对象serviceB,此时就尝试去get(B),发现B还没有被实create,所以走create流程;
5>serviceB完成了初始化的第一步(实例化:调用createBeanInstance方法,即调用默认构造方法实例化);
6>将自己(serviceB)提前曝光到singletonFactories中;
7>此时进行初始化的第二步(注入属性serviceA);
8>于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀);
9>B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中;
10>此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中;

知道了这个原理时候,肯定就知道为啥Spring不能解决“A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象”这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。

4、循环依赖的N种场景

这里主要讲解spring是如何解决单例setter注入时的循环依赖问题,其他四种循环依赖场景后文再一一分析讲解

3.1、单例的setter注入
这种注入方式应该是spring用的最多的,代码如下:

@Service
public class ServiceA {@Autowiredprivate ServiceB serviceB;
}@Service
public class ServiceB {@Autowiredprivate ServiceA serviceA;
}


spring内部有三级缓存:

singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。
下面用一张图告诉你,spring是如何解决循环依赖的:

上面(3、Spring怎么解决循环依赖)中已经讲解过spring解决单例循环依赖的过程,这里细心的朋友可能会发现在这种场景中第二级缓存(earlySingletonObjects)作用不大。
那么问题来了,为什么要用第二级缓存呢?
试想一下,如果出现以下这种情况,我们要如何处理?

@Service
public class ServiceA {@Autowiredprivate ServiceB serviceB;@Autowiredprivate ServiceB serviceC;
}@Service
public class ServiceB {@Autowiredprivate ServiceA serviceA;
}@Service
public class ServiceC {@Autowiredprivate ServiceA serviceA;
}


serviceA依赖于serviceB和serviceC,而serviceB依赖于serviceA,同时serviceC也依赖于serviceA。按照上图的流程可以把serviceA注入到serviceB,并且sServiceA的实例是从第三级缓存中获取的。假设不用第二级缓存,serviceA注入到serviceC的流程如图:
图2

 

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

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

还有个问题,第三级缓存中为什么要添加ObjectFactory对象,直接保存实例对象不行吗?
答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。
针对这种场景spring是怎么做的呢?
答案就在AbstractAutowireCapableBeanFactory类doCreateBean方法的这段代码中:
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");
    }
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

它定义了一个匿名内部类,通过getEarlyBeanReference方法获取代理对象,其实底层是通过AbstractAutoProxyCreator类的getEarlyBeanReference生成代理对象。

3.2、多例的setter注入
这种注入方法偶然会有,具体代码如下:

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceA {@Autowiredprivate ServiceB serviceB;
}@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceB {@Autowiredprivate ServiceA serviceA;
}


可能你会认为这种情况spring容器启动会报错,其实是不会,为什么呢?其实在AbstractApplicationContext类的refresh方法中告诉了我们答案,它会调用finishBeanFactoryInitialization方法,该方法的作用是为了spring容器启动的时候提前初始化一些bean。该方法的内部又调用了beanFactory.preInstantiateSingletons()方法:

public ConfigurableApplicationContext run(String... args) {StopWatch stopWatch = new StopWatch();stopWatch.start();ConfigurableApplicationContext context = null;Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();this.configureHeadlessProperty();SpringApplicationRunListeners listeners = this.getRunListeners(args);listeners.starting();Collection exceptionReporters;try {ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);this.configureIgnoreBeanInfo(environment);Banner printedBanner = this.printBanner(environment);context = this.createApplicationContext();exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);this.refreshContext(context);this.afterRefresh(context, applicationArguments);stopWatch.stop();if (this.logStartupInfo) {(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);}listeners.started(context);this.callRunners(context, applicationArguments);} catch (Throwable var10) {this.handleRunFailure(context, var10, exceptionReporters, listeners);throw new IllegalStateException(var10);}try {listeners.running(context);return context;} catch (Throwable var9) {this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);throw new IllegalStateException(var9);}}

 

refresh:

public void refresh() throws BeansException, IllegalStateException {synchronized(this.startupShutdownMonitor) {this.prepareRefresh();ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();this.prepareBeanFactory(beanFactory);try {this.postProcessBeanFactory(beanFactory);this.invokeBeanFactoryPostProcessors(beanFactory);this.registerBeanPostProcessors(beanFactory);this.initMessageSource();this.initApplicationEventMulticaster();this.onRefresh();this.registerListeners();this.finishBeanFactoryInitialization(beanFactory);this.finishRefresh();} catch (BeansException var9) {if (this.logger.isWarnEnabled()) {this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);}this.destroyBeans();this.cancelRefresh(var9);throw var9;} finally {this.resetCommonCaches();}}}

 

    protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {if (beanFactory.containsBean("conversionService") && beanFactory.isTypeMatch("conversionService", ConversionService.class)) {beanFactory.setConversionService((ConversionService)beanFactory.getBean("conversionService", ConversionService.class));}if (!beanFactory.hasEmbeddedValueResolver()) {beanFactory.addEmbeddedValueResolver((strVal) -> {return this.getEnvironment().resolvePlaceholders(strVal);});}String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);String[] var3 = weaverAwareNames;int var4 = weaverAwareNames.length;for(int var5 = 0; var5 < var4; ++var5) {String weaverAwareName = var3[var5];this.getBean(weaverAwareName);}beanFactory.setTempClassLoader((ClassLoader)null);beanFactory.freezeConfiguration();beanFactory.preInstantiateSingletons();}

 

红框的地方明显能够看出:非抽象、单例 并且非懒加载的类才能被提前初始bean。而多例即SCOPE_PROTOTYPE类型的类,非单例,不会被提前初始化bean,所以程序能够正常启动。如何让它提前初始化bean呢?只需要再定义一个单例的类,在它里面注入serviceA: 

@Service
public class ServiceC {@Autowiredprivate ServiceA serviceA;
}

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

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

果然出现了循环依赖。

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

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

@Service
public class ServiceA {public ServiceA(ServiceB serviceB) {}
}@Service
public class ServiceB {public ServiceB(ServiceA serviceA) {}
}

运行结果:

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

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

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

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

@Service
@EnableAsync
public class ServiceA {@Autowiredprivate ServiceB serviceB;@Asyncpublic void test1() {System.out.println("async");}
}@Service
public class ServiceB {@Autowiredprivate ServiceA serviceA;
}



程序启动会报错,出现了循环依赖:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘serviceA’: Bean with name ‘serviceA’ has been injected into other beans [serviceB] in its raw version as part of a circular reference

为什么会循环依赖呢?答案就在下面这张图中:

 doCreateBean

 protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {BeanWrapper instanceWrapper = null;if (mbd.isSingleton()) {instanceWrapper = (BeanWrapper)this.factoryBeanInstanceCache.remove(beanName);}if (instanceWrapper == null) {instanceWrapper = this.createBeanInstance(beanName, mbd, args);}Object bean = instanceWrapper.getWrappedInstance();Class<?> beanType = instanceWrapper.getWrappedClass();if (beanType != NullBean.class) {mbd.resolvedTargetType = beanType;}synchronized(mbd.postProcessingLock) {if (!mbd.postProcessed) {try {this.applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);} catch (Throwable var17) {throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Post-processing of merged bean definition failed", var17);}mbd.postProcessed = true;}}boolean earlySingletonExposure = mbd.isSingleton() && this.allowCircularReferences && this.isSingletonCurrentlyInCreation(beanName);if (earlySingletonExposure) {if (this.logger.isTraceEnabled()) {this.logger.trace("Eagerly caching bean '" + beanName + "' to allow for resolving potential circular references");}this.addSingletonFactory(beanName, () -> {return this.getEarlyBeanReference(beanName, mbd, bean);});}Object exposedObject = bean;try {this.populateBean(beanName, mbd, instanceWrapper);exposedObject = this.initializeBean(beanName, exposedObject, mbd);} catch (Throwable var18) {if (var18 instanceof BeanCreationException && beanName.equals(((BeanCreationException)var18).getBeanName())) {throw (BeanCreationException)var18;}throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Initialization of bean failed", var18);}if (earlySingletonExposure) {Object earlySingletonReference = this.getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) {exposedObject = earlySingletonReference;} else if (!this.allowRawInjectionDespiteWrapping && this.hasDependentBean(beanName)) {String[] dependentBeans = this.getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet(dependentBeans.length);String[] var12 = dependentBeans;int var13 = dependentBeans.length;for(int var14 = 0; var14 < var13; ++var14) {String dependentBean = var12[var14];if (!this.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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");}}}}try {this.registerDisposableBeanIfNecessary(beanName, bean, mbd);return exposedObject;} catch (BeanDefinitionValidationException var16) {throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid destruction signature", var16);}}

说白了,bean初始化完成之后,后面还有一步去检查:第二级缓存是否存在, 代理对象和原始对象是否相等。

到此发现第二级缓存存在并且代理对象和原始对象是不相等,因此抛出上面异常。

如果这时候把ServiceA改个名字,改成:ServiceC,其他的都不变。

@Service
public class ServiceB {@Autowiredprivate ServiceC serviceC;
}@Service
@EnableAsync
public class ServiceC {@Autowiredprivate ServiceB serviceB;@Asyncpublic void test1() {System.out.println("async");}
}

 再重新启动一下程序,成功了,没有报错了!!!
这又是为什么?
这就要从spring的bean加载顺序说起了,默认情况下,spring是按照文件完整路径递归查找的,按路径+文件名排序,排在前面的先加载。所以ServiceA比ServiceB先加载,而改了文件名称之后,ServiceB比ServiceC先加载。为什么ServiceB比ServiceC先加载就没问题呢?答案在下面这张图中

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

3.5、DependsOn循环依赖

@Service
@DependsOn("serviceB")
public class ServiceA {}@Service
@DependsOn("serviceA")
public class ServiceB {}

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

Circular depends-on relationship between ‘serviceB’ and ‘serviceA’

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

这又是为什么?

答案在AbstractBeanFactory类的doGetBean方法的这段代码中:

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

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

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

5.1、生成代理对象产生的循环依赖
生成代理对象产生的循环依赖这类循环依赖问题解决方法很多,主要有:
1、使用@Lazy注解,延迟加载
2、使用@DependsOn注解,指定加载先后关系
3、修改文件名称,改变循环依赖类的加载顺序

5.2、DependsOn循环依赖
使用@DependsOn产生的循环依赖这类循环依赖问题要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题。

5.3、多例循环依赖
多例循环依赖这类循环依赖问题可以通过把bean改成单例的解决。

5.3、构造器循环依赖
构造器循环依赖这类循环依赖问题可以通过使用@Lazy注解解决。
 

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

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

相关文章

java获取异常堆栈详情

/** * 获取exception详情信息 * * param e * Excetipn type * return String type */ public static String getExceptionDetail(Exception e) { StringBuffer msg new StringBuffer("null"); if (e ! null) { msg new StringBuffer(""); String messag…

多线程间共享变量线程安全问题——ThreadLocal

Java并发编程中很重要的类&#xff1a;ThreadLocal 在多线程应用程序中&#xff0c;对共享变量进行读写的场景是很常见的。如果不使用一定的技术或方案&#xff0c;会引发各种线程安全的问题。常见解决线程安全的方式有synchronized、volatile等方式&#xff0c;但synchronized…

我的学生时代之一[小学时代]

真是岁月如梭~ 猛然间这么一掐指&#xff0c;我都毕业4年有余了。先前工作一直不如意&#xff0c;到现在好一点点。 有时候一想&#xff0c;挺怀念上学的时光。 真的是回不去了~ 我又记得多少同学的名字&#xff1f;朋友还有多少常联系的&#xff1f;不很多~ 今天&#xff0c;突…

java8流式操作

简介&#xff1a;Stream 中文称为 “流”&#xff0c;通过将集合转换为这么一种叫做 “流” 的元素序列&#xff0c;通过声明性方式&#xff0c;能够对集合中的每个元素进行一系列并行或串行的流水线操作。 操作分类&#xff1a; .stream() stream()把一个源数据&#xff0c;可…

web.config中httpRunTime的属性

配置httpRuntime也可以让FileUpload上传更大的文件,不过设置太大了会因用户将大量文件传递到该服务器而导致的拒绝服务攻击(属性有说明) <httpRuntime> <httpRuntime useFullyQualifiedRedirectUrl"true|false" maxRequestLength"size in kbytes"…

创建并运用客户化jsp标签

1.在WEB-INF目录下新建message.properties属性文件 文件内容为“key-value”对&#xff0c;添加测试内容如下&#xff1a;titlehello world bodyhello taglib 2.定义初始化类TaglibInit&#xff0c;用…

ArrayList源码阅读

private static void extracted() {ArrayList<StudentVO> arrayList new ArrayList<StudentVO>();arrayList.add(new StudentVO("张三", 23));arrayList.add(new StudentVO("李四", 24));arrayList.add(new StudentVO("王五", 24))…

常用的JS小功能整理

<a href"#" onclick "this.style.behaviorurl(#default#homepage);this.sethomepage(http://www.mingrisoft.com)" style" color:Black; font-size: 9pt; font-family: 宋体; text-decoration :none;" >设置主页</a> <a href&quo…

类的加载过程

类的加载过程 代码 public class Father{private int i test();private static int j method();static{System.out.print("(1)");}Father(){System.out.print("(2)");}{System.out.print("(3)");)public int test(){System.out.print("(…

微软企业库调用Oracle分页存储过程

存储过程&#xff1a;CREATE OR REPLACE PACKAGE pkg_tableTypeIS procedure FY( TableName varchar2, -- 表名getFields varchar2, -- 字段名(全部字段为*) OrderField varchar2, -- 排序字段(必须!支持多字段) whereCondition varchar2, -- 条件语句(不用加where) pageSize i…

Windows服务无法引用.dll的错误

项目中需要使用.NET开发Windows服务来检测MSMQ&#xff0c;但一直无法引用.dll(特别是.dll引用了其它的.dll)&#xff0c;最后google找到了答案&#xff1a; Every window service project, by default targets to .netClient version (which is not full version of .net and …

TC第一次成为room leader

虽然第二题竟然最后没通过system test&#xff0c;用递归的方法超时了 还好challenge 3个&#xff0c;以微弱优势胜过第二名 happy&#xff01; 继续努力转载于:https://www.cnblogs.com/fstang/archive/2012/12/21/2827345.html

[C/C++]BKDRHash

将字符串Hash成整型存储经常用到BKDRHash算法 uint64_t BKDRHash(const char *pszKey) {uint64_t seed 131;register uint64_t uCode0;while(pszKey[0]){uCode uCode *seed (unsigned char)pszKey[0];pszKey;}return uCode; }选择了64位的key&#xff0c;减少冲突的概率。转…

教你如何开发一个 SpringBoot starter

从前从前&#xff0c;有个面试官问我一个 SpringBoot Starter 的开发流程&#xff0c;我说我没有写过 starter&#xff0c;然后就没有然后了&#xff0c;面试官说我技术深度不够。 我想说这东西不是很简单吗&#xff0c;如果要自己写一个出来也是分分钟的事情。至于就因为我没…

nginx 搭建http协议拖动播放 FLV 视频播放服务器

原创作品&#xff0c;允许转载&#xff0c;转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。http://deidara.blog.51cto.com/400447/235562所需要的 播放器&#xff0c;我用的开源的 JW FLV Media Player我把我的上传到了blog 大家可以下载…

两分钟彻底让你明白Android Activity生命周期(图文)!

转&#xff1a;http://blog.csdn.net/qyf_5445/article/details/8290232 首先看一下Android api中所提供的Activity生命周期图(不明白的&#xff0c;可以看完整篇文章&#xff0c;在回头看一下这个图&#xff0c;你会明白的): Activity其实是继承了ApplicationContext这个类&am…

spring4和spring5的aop执行顺序区别?

spring4单切面 spring4多切面 spring4 spring5

jquery datepicker 点击日期控件不会自动更新input的值

页面代码&#xff1a;<link href"http://code.jquery.com/ui/1.9.2/themes/base/jquery-ui.css" rel"stylesheet" type"text/css"/> <link href"/static/css/main.css" rel"stylesheet" type"text/css"/…

ArcGIS API for Silverlight中legend控件显示图例问题

转自http://www.gisall.com/html/34/9534-5141.html 在使用ArcGIS API for Silverlight进行地图展示应用的时候&#xff0c;我们都会设置地图图层列表的图例&#xff08;该图例包含有图层名称和图层符号&#xff09;&#xff0c;但是在使用API时却出现了图例无法正常显示&#…