侧边栏壁纸
博主头像
术业有道之编程博主等级

亦是三月纷飞雨,亦是人间惊鸿客。亦是秋霜去叶多,亦是风华正当时。

  • 累计撰写 99 篇文章
  • 累计创建 50 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

说说spring中的循环依赖

Administrator
2020-09-01 / 0 评论 / 0 点赞 / 55 阅读 / 4514 字

目前正在基于java实现循环依赖,于是乎参考了spring的循环依赖注入的方案,也看了很多博客,都是侃侃而谈,在这里我说说我是如何在spring外解决循环依赖的。

循环依赖有2种:

1、构造方法依赖

2、属性依赖

第一种方法无解,违反了java方法调用的原则。这里我只说第二种。

先简要的说一下类是如何被创建的:

1、使用加载器类的class文件进jvm,获得一个 Class<?> 对象

2、jvm调用第一步产生的Class<?> 对象中的默认构造器进行初始化,获得一个Object对象,此时对象已经分配内存地址,已经具备身份标识和持有能力,但是此时这个Object对象的属性都是空的,因为还没有调用get set 等方法,仅仅只是调用了默认构造方法(这也就是为什么一个类会有默认构造方法的原因)。

注意:到这里其实一个类产生已经完成,我们常规 new 一个model 也就是到这里为止了。以下是我实现的过程了。

3、检测第一步产生的Class<?>对象的 DeclaredFields (如果用字节码生成的实际上是 Superclass的DeclaredFields)中是否带有注入注解,即是否需要注入外部资源。不需要即初始化完成。

4、需要注入外部资源时,去匹配要注入的资源Class 根据 Class去已经初始化好的Object池(BeanFactory)中寻找现成的Object对象。这里涉及到了2个东西BeanFactory 与Scope(prototype,singleton)。当目标资源标记为多实例时,会直接创建一个新的Object。默认为singleton,即单实例。

注意:属性循环依赖就发生在这第四步上。好了回到正题上

一、什么是属性循环依赖?

即A类 有个 Field 是B类 ,B类有个 Field 是C类 ,C类有个 Field 是A类 ,如此构成循环,当然2级即可形成依赖。

二、属性循环依赖产生的原因?

java在创建一个对象的时候使用构造方法进行初始化对象,因为这一步是必须,且必须是在第一步完成的,所以循环依赖的构造方法依赖是无法解决的,没有实参是调用不了构造方法的。

不扯远了,继续说属性循环依赖发生的过程:

第一步:A类调用构造方法初始化之后,分配了内存地址,已经能被人认识和持有了,A继续完善自己属性的时候发现需要B的资源,于是去加载B的class文件(也有可能这个class被提前加载过,这里不展开了有兴趣的可以看看双亲委派模型)

第二步:调用B的构造方法初始化B,这时候B也分配了内存地址,能被人认识和持有了,B发现继续完善自己属性的时候需要C的资源,于是去加载C的class文件

第三步:调用C的构造方法初始化C,这时候C也分配了内存地址,能被人认识和持有了,C发现继续完善自己属性的时候需要A的资源,于是去加载A的class文件

开始循环:C发现A的class已经被加载,继续获取A的过程中发现A在等待其他资源,于是乎这三个对象都开始了等待过程,现象非常像死锁。

注意:这里涉及到Scope(prototype,singleton),如果是singleton,这里会造成循环持有引用;如果是prototype,这里会以第三步为起点重新开始走123步,直到方法栈帧溢出。

三、如何解决属性循环依赖?

解决属性循环依赖分2种情况:

1、Scope为singleton:spring使用了三级缓存,我不打算在这里说三级缓存,有兴趣的自己百度一大堆。

上面说到singleton时会造成循环持有引用,这里开始产生了java的特性,即每个人都持有一个人的引用,那么就可以利用持有引用的特性打破循环。

具体操作如下:

现象:A持有B的引用,B持有C的引用,C持有A的引用。

1、现在我告诉A(因为A是产生循环的起点,解铃还须系铃人)说你卖我个面子,我现在欠你一个完整的B但是马上你就有完整的B了,你先跟大家说你现在已经是可用的了,于是乎A把自己放到了一个缓存区中。

2、C在之前就已经得到了A的引用,现在去缓存区得到了A于是C马上也变成了可用,C知道自己可用了之后,也把自己放到了这个缓存区。

3、于是乎B也发现了C,B做了和C相同的操作。

4、A发现B已经在可用状态了,于是去缓存区得到了B,感觉你说话很靠谱,值得信赖。

注意:第4步不是必须的,因为A并不会去核实第1步的过程。

当C填充了A之后 B持有C的引用,B里面的C其实就是外面的C。

举个例子:

C中有个属性 String a=null, 当C把这个属性改成了 String a="你好" ,持有C属性的人都会改变了,本身他们持有的引用就是同一个,说深一点就到了jvm内存模型的层面了,这里不展开讨论了。

2、Scope为prototype:如果每次在A B C的节点上开辟新的ABC循环,这样会非常快的导致方法帧栈溢出,所以,当为prototype时,也是走的singleton,等大家都依赖注入完成了之后,获得这个prototype的bean时进行一个copy或者重新走一遍注入流程即可,因为所有的资源都已经被依赖注入完成了,这时候在次进行并不会产生循环。

这里我其实也不清楚spring用的是copy 还是重新走一遍,如果是我,我会选择copy的方式,比较重新走一遍,如果ABC全部是prototype,那要么回到之前的蛋疼循环依赖,要么用copy解决,何不一开始就用copy呢。

四、解决属性循环依赖就完了么?

解决了循环依赖只是解决了问题的一半,如果方法调用链发生循环呢?

但是发生属性循环依赖注入后,在方法调用链上通常有2种情况:

1、循环依赖的对象中调用了依赖对象的方法,发生了方法调用链循环。

例如:A类有一个方法需要B类的out方法,而B类的out方法需要C类的out方法,而C类的out方法需要A类的out方法。

这种方式导致方法栈帧没有出口的递归循环的压数据到方法栈帧里,必然会导致jvm方法栈帧溢出,而且无解。

2、循环依赖的对象中没有循环调用方法,没有发生方法调用链循环。

例如:A类有一个方法需要B类的out方法,而B类的out方法需要C类的out方法,而C类的out方法不需要A类的out方法。

这种方式在C类有个出口,没有对A形成闭环,所以是没有问题的。

注意:2种方式唯一的不同是方法调用链没有形成闭环。

从jvm内存模型上说说第一种问题:

当A调用B中的方法时,A中发起调用的方法栈帧会开始申请内存区域来存放运行这个方法所需要的资源,发现需要B的某个方法,于是从内存中加载了B方法运行需要的资源,加载的过程中发现B方法需要C方法的资源,于是去加载C方法资源,加载C方法资源需要A方法资源,于是开始加载A方法资源。这种与属性循环依赖是一样的,只是很不幸运,要解决这个问题不单单是引用能搞定的,也需要一个特定机制来保障调用,否则最终出现方法栈帧溢出。

就目前来讲没有办法解决这个问题,我个人设想,尝试使用字节码做动态切入,外部机制检测方法调用链循环用切点切断这个循环,然后在依赖的地方做一个偷梁换柱,只是这样需要抽离所有参与调用链方法的外部方法资源,然后做一个方法级别的切入替换,然而这个过程也只能纸上谈兵,能不能实现还不好说。

结尾:

属性循环依赖注入是解决了,但是实质上是没有解决本质问题,因为你会发现,调用一个具有循环依赖的类方法时会出现方法栈帧溢出。当然从框架层面它是没有锅的,已经做了最好的兼容,实在搞事情,那是程序员的问题,总不能让框架去帮程序员写代码吧。所以程序员才是解决循环注入的关键。

说回到spring上,同样spring解决了属性循环依赖注入的问题,但是对极有可能产生的方法循环调用的问题是无法解决的。

好了,说了这么多一句代码都没有,看这里:

以上实现的具体源代码地址:https://github.com/liuchengts/bassis/tree/boot-1.0/bassis-bean/src/main/java/com/bassis/bean

欢迎关注我的个人公众号
个人公众号

0

评论区