辽宁省建设工程招标投标协会网站,蜜雪冰城网站建设策划方案,商洛网站开发公司,简历中建设网站的项目经历文章目录1.前言2.什么是循环依赖#xff1f;3.两种Spring容器循环依赖3.1.构造器循环依赖#xff08;无法解决#xff09;3.2.setter循环依赖#xff08;可以解决#xff09;3.3.小结4.循环依赖检查5.循环依赖的处理5.1.单例setter循环依赖5.2.Spring解决循环依赖5.3. 循环…
文章目录1.前言2.什么是循环依赖3.两种Spring容器循环依赖3.1.构造器循环依赖无法解决3.2.setter循环依赖可以解决3.3.小结4.循环依赖检查5.循环依赖的处理5.1.单例setter循环依赖5.2.Spring解决循环依赖5.3. 循环依赖的本质5.4.what问题的本质居然是two sum5.5.单例构造器注入循环依赖5.6.原型模式循环依赖6.总结6.1.循环依赖的原因6.2.循环依赖的解决方案6.3.整个从创建bean到解决循环依赖的过程6.4.循环依赖建议7.Spring循环依赖图解1.前言
想彻底弄清楚spring的循环依赖问题首先得弄清楚
循环依赖是如何发生的spring又是如何检测循环依赖的发生的。其次再探究spring如何解决循环依赖的问题
2.什么是循环依赖
循环依赖就是循环引用指两个或多个bean互相持有对方比如说TestA引用TestB、TestB引用TestA最终形成一个闭环。 注意循环依赖不是指循环调用。 循环调用指方法之间的环调用循环调用是无解的除非有终结条件否则就是死循环最终会导致内存溢出异常。 package com.bruce.test;import java.io.File;public class TestFile {static int count0;public static void main(String[] args) {File fnew File(D:\\);showFiles(f);System.out.println(文件总数是:count);}public static void showFiles(File f){//获取所有的文件和文件目录File[] files f.listFiles();for (File file : files) {if(file.isFile()){if(file.getName().endsWith(.jpg)){count;System.out.println(file.getAbsolutePath());}}else{//目录showFiles(file);}}}
}
3.两种Spring容器循环依赖
构造器循环依赖setter方法循环依赖
就是A类里面有B类的引用bB类里面有C类的引用cC类里面有A类的引用a如下图所示
3.1.构造器循环依赖无法解决
A类
package com.bruce.spring2021;public class A {private B b;public A (B b){this.b b;}public B getB() {return b;}public void setB(B b) {this.b b;}
}
B类
package com.bruce.spring2021;public class B {private C c;public B (C c){this.c c;}public C getC() {return c;}public void setC(C c) {this.c c;}
}
C类
package com.bruce.spring2021;public class C {private A a;public C (A a){this.a a;}public A getA() {return a;}public void setA(A a) {this.a a;}
}
applicationContext.xml配置文件
?xml version1.0 encodingUTF-8?
beans xmlnshttp://www.springframework.org/schema/beansxmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdbean idA classcom.bruce.spring2021.A constructor-arg nameb refB//beanbean idB classcom.bruce.spring2021.B constructor-arg namec refC//beanbean idC classcom.bruce.spring2021.C constructor-arg namea refA//bean/beans主函数入口
package com.bruce.spring2021;import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class TestSpring {Testpublic void test1(){ClassPathXmlApplicationContext ctx new ClassPathXmlApplicationContext(applicationContext.xml);}}运行main函数报错截图
3.2.setter循环依赖可以解决
A类
public class A {public A() {}private B b;public B getB() {return b;}public void setB(B b) {this.b b;}
}
B类
public class B {public B() {}private C c;public C getC() {return c;}public void setC(C c) {this.c c;}
}
C类
public class C {public C() {}private A a;public A getA() {return a;}public void setA(A a) {this.a a;}
}applicationContext.xml配置文件
?xml version1.0 encodingUTF-8?
beans xmlnshttp://www.springframework.org/schema/beansxmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdbean idA classcom.bruce.spring2021.A property nameb refB//beanbean idB classcom.bruce.spring2021.B property namec refC//beanbean idC classcom.bruce.spring2021.C property namea refA//bean/beans最后输出
package com.bruce.spring2021;import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class TestSpring {Testpublic void test2(){ClassPathXmlApplicationContext ctx new ClassPathXmlApplicationContext(applicationContext1.xml);A a ctx.getBean(A, A.class);//System.out.println(a);//System.out.println(a.getB());//System.out.println(a.getB().getC());//System.out.println(a.getB().getC().getA());}
}3.3.小结
通常来说如果问Spring内部如何解决循环依赖一定是单默认的单例Bean中属性互相引用的场景。比如几个Bean之间的互相引用
甚至自己“循环”依赖自己 先说明前提原型(Prototype)的场景是不支持循环依赖的通常会走到AbstractBeanFactory类中下面的判断抛出异常
if (isPrototypeCurrentlyInCreation(beanName)) {throw new BeanCurrentlyInCreationException(beanName);
}原因很好理解创建新的A时发现要注入原型字段B又创建新的B发现要注入原型字段A…
这就套娃了, 你猜是先StackOverflow还是OutOfMemory
Spring怕你不好猜就先抛出了BeanCurrentlyInCreationException
基于构造器的循环依赖就更不用说了官方文档都摊牌了你想让构造器注入支持循环依赖是不存在的不如把代码改了。
那么默认单例的属性注入场景Spring是如何支持循环依赖的
4.循环依赖检查
bean ida classAproperty nameb refb
bean/bean idb classBproperty namea refa
bean/无论单例还是原型模式(下文①代表图中步骤1)spring都有对应的集合(singletonsCurrentlyInCreation)保存当前正在创建的beanName标识该beanName正在被创建。
在bean创建前 ①检测当前bean是否在创建中如果不在创建中则 ②将beanName加入集合往下创建bean。在bean创建前检测到当前的bean正在创建则说明发生循环依赖抛出异常。最后记得当bean创建完时将beanName移出集合。 5.循环依赖的处理
5.1.单例setter循环依赖
spring注入属性的方式有多种但是只有一种循环依赖能被解决单例setter依赖注入。 spring解决循环依赖的做法是未等bean创建完就先将实例曝光出去方便其他bean的引用。同时还提到了三级缓存最先曝光到第三级缓存singletonFactories中。简单的说就是spring先将创建好的实例放到缓存中让其他bean可以提前引用到该对象。 示例
// 第一种 注解方式
public class A {Autowiredprivate B b;
}public class B {Autowiredprivate A a;
}//
// 第二种 xml配置方式
public class A {private B b;// getter setter
}public class B {private A a;// getter setter
}bean ida classAproperty nameb refb
bean/bean idb classBproperty namea refa
bean/
分析 其中跟循环依赖检测对比新添加的几个关键节点已经用黄色标识出来这里有几个重点给大家画一下。 提前曝光如果用c语言的说法就是将指针曝光出去用java就是将引用对象曝光出去。也就是说即便a对象还未创建完成但是在④实例化过程中new A()动作已经开辟了一块内存空间只需要将该地址抛出去b就可以引用的到而不管a后期还会进行初始化等其他操作 已经了解了提前曝光的作用而相比而言⑤曝光的时机也非常的重要该时机发生在④实例化之后⑥填充与⑯ 初始化之前。spring循环依赖之所以不能解决实例化注入的原因正式因为注入时机在曝光之前所导致
5.2.Spring解决循环依赖
首先Spring内部维护了三个Map也就是我们通常说的三级缓存。 在Spring的DefaultSingletonBeanRegistry类中你会赫然发现类上方挂着这三个Map
singletonObjects 它是我们最熟悉的朋友俗称“单例池”“容器”缓存创建完成单例Bean的地方。singletonFactories 映射创建Bean的原始工厂earlySingletonObjects 映射Bean的早期引用也就是说在这个Map里的Bean不是完整的甚至还不能称之为“Bean”只是一个Instance.
后两个Map其实是“垫脚石”级别的只是创建Bean的时候用来借助了一下创建完成就清掉了。
为什么成为后两个Map为垫脚石假设最终放在singletonObjects的Bean是你想要的一杯“凉白开”。 那么Spring准备了两个杯子即singletonFactories和earlySingletonObjects来回“倒腾”几番把热水晾成“凉白开”放到singletonObjects中。 闲话不说都浓缩在图里。 5.3. 循环依赖的本质
上文了解完Spring如何处理循环依赖之后让我们跳出“阅读源码”的思维假设让你实现一个有以下特点的功能你会怎么做
将指定的一些类实例为单例类中的字段也都实例为单例支持循环依赖
举个例子假设有类A
Component
public class A {Autowiredprivate B b;
}
// 类BComponent
public class B {Autowiredprivate A a;Transactionpublic void show(){}
}说白了让你模仿Spring假装A和B是被Component修饰 并且类中的字段假装是Autowired修饰的处理完放到Map中。其实非常简单笔者写了一份粗糙的代码可供参考 /*** 放置创建好的bean Map*/private static MapString, Object cacheMap new HashMap(2);public static void main(String[] args) {// 假装扫描出来的对象Class[] classes {A.class, B.class};// 假装项目初始化实例化所有beanfor (Class aClass : classes) {getBean(aClass);}// checkSystem.out.println(getBean(B.class).getA() getBean(A.class));System.out.println(getBean(A.class).getB() getBean(B.class));}SneakyThrowsprivate static T T getBean(ClassT beanClass) {// 本文用类名小写 简单代替bean的命名规则String beanName beanClass.getSimpleName().toLowerCase();// 如果已经是一个bean则直接返回if (cacheMap.containsKey(beanName)) {return (T) cacheMap.get(beanName);}// 将对象本身实例化Object object beanClass.getDeclaredConstructor().newInstance();// 放入缓存cacheMap.put(beanName, object);// 把所有字段当成需要注入的bean创建并注入到当前bean中Field[] fields object.getClass().getDeclaredFields();for (Field field : fields) {field.setAccessible(true);// 获取需要注入字段的classClass? fieldClass field.getType();String fieldBeanName fieldClass.getSimpleName().toLowerCase();// 如果需要注入的bean已经在缓存Map中那么把缓存Map中的值注入到该field即可// 如果缓存没有 继续创建field.set(object, cacheMap.containsKey(fieldBeanName)? cacheMap.get(fieldBeanName) : getBean(fieldClass));}// 属性填充完成返回return (T) object;}这段代码的效果其实就是处理了循环依赖并且处理完成后cacheMap中放的就是完整的“Bean”了 这就是“循环依赖”的本质而不是“Spring如何解决循环依赖”。
5.4.what问题的本质居然是two sum
看完笔者刚才的代码有没有似曾相识没错和two sum的解题是类似的。不知道two sum是什么梗的笔者和你介绍一下two sum是刷题网站leetcode序号为1的题也就是大多人的算法入门的第一题。常常被人调侃有算法面的公司被面试官钦定了合的来。那就来一道two sum走走过场。
问题内容是给定一个数组给定一个数字。返回数组中可以相加得到指定数字的两个索引。比如给定
nums [2, 7, 11, 15],
target 9 那么要返回 [0, 1]因为2 7 9这道题的优解是一次遍历HashMap
class Solution {public int[] twoSum(int[] nums, int target) {MapInteger, Integer map new HashMap();for (int i 0; i nums.length; i) {int complement target - nums[i];if (map.containsKey(complement)) {return new int[] { map.get(complement), i };}map.put(nums[i], i);}throw new IllegalArgumentException(No two sum solution);}
}先去Map中找需要的数字没有就将当前的数字保存在Map中如果找到需要的数字则一起返回。
和笔者上面的代码是不是一样
先去缓存里找Bean没有则实例化当前的Bean放到Map如果有需要依赖当前Bean的就能从Map取到。
5.5.单例构造器注入循环依赖
上面已经剧透了这个方式是不得行的原因是依赖注入的时间点不对他的依赖注入发生在构造器阶段这个时候连实例都没有内存都还没开辟完当然也还没有进行提前曝光因此不得行
示例
public class A {private B b;Autowiredpublic A(B b) {this.b b;}
}public class B {private A a;Autowiredpublic B(A a) {this.a a}
}
分析 图上重点地方也用黄色标出了问题的原因处在④实例化实例化的过程是调用new A(B b);的过程这时的A还未创建出来根本是不可能提前曝光的正是这个原因导致⑨无法获取到三级缓存进而导致⑩异常的抛出
5.6.原型模式循环依赖
这此没有图了因为原型模式每次都是重新生成一个全新的bean根本没有缓存一说。这将导致实例化A完填充发现需要B实例化B完又发现需要A而每次的A又都要不一样所以死循环的依赖下去。唯一的做法就是利用循环依赖检测发现原型模式下存在循环依赖并抛出异常.
6.总结
6.1.循环依赖的原因
beanA依赖beanBbeanB依赖beanA导致两者都不能被创建发生在填充属性的环节
6.2.循环依赖的解决方案
提前曝光机制三级缓存
提前曝光 正常来说bean的创建过程有三步实例化-填充属性-初始化提前曝光就是实例化后填充属性前将bean放入缓存 总结一下循环依赖spring只能解决setter注入单例模式下的循环依赖问题。要想解决循环依赖必须要满足2个条件
需要用于提前曝光的缓存属性的注入时机必须发生在提前曝光动作之后不管是填充还是初始化都行总之不能在实例化因为提前曝光动作在实例化之后
理解了这2点就可以轻松驾驭循环依赖了。比如构造器注入是不满足第二个条件曝光时间不对。而原型模式则是缺少了第一个条件没有提前曝光的缓存供使用。
三级缓存
singletonObjects一级缓存用于存放完全初始化好的bean从改缓存中取出的bean可以直接使用
earlySingletonObjects二级缓存用于存放提前曝光的单例对象的cache存放原始的bean对象尚未填充属性
singletoneFactories三级缓存存放提前曝光的bean的工厂用于生产二级缓存提前曝光的实例urrentlyInCreation正在创建的bean集合在bean开始创建时放值创建完成时移出
alreadyCreatedbean被创建完成后会放进这个set集合先从一级缓存获取bean-从二级缓存获取-如果工厂可以创建bean就从三级缓存获取并且将三级缓存中的bean移到二级缓存
public interface ObjectFactoryT {T getObject() throws BeansException;
}加入singletonFactories三级缓存的前提是执行了构造器所以构造器的循环依赖没法解决
6.3.整个从创建bean到解决循环依赖的过程
context.getBean(A.class)-实例化-放入缓存-依赖注入B-getBean(B)-实例化B并放入缓存-B依赖注入A-getBean(A)获取到了缓存中的值并正常返回-B初始化成功-A初始化成功
6.4.循环依赖建议
业务代码中尽量不要使用构造器注入三级缓存解决不了构造器循环依赖的问题
业务代码中为了简洁尽量使用field注入而不是setter方法注入
7.Spring循环依赖图解