共计 6643 个字符,预计需要花费 17 分钟才能阅读完成。
1. 由共事抛的一个问题开始
最近项目组的一个共事遇到了一个问题,问我的意见,一下子引起的我的趣味,因为这个问题我也是第一次遇到。平时自认为对 spring 循环依赖问题还是比拟理解的,直到遇到这个和前面的几个问题后,从新刷新了我的意识。
咱们先看看过后出问题的代码片段:
@Service
publicclass TestService1 {
@Autowired
private TestService2 testService2;
@Async
public void test1() {}
}
@Service
publicclass TestService2 {
@Autowired
private TestService1 testService1;
public void test2() {}
}
这两段代码中定义了两个 Service 类:TestService1 和 TestService2,在 TestService1 中注入了 TestService2 的实例,同时在 TestService2 中注入了 TestService1 的实例,这里形成了循环依赖。
只不过,这不是一般的循环依赖,因为 TestService1 的 test1 办法上加了一个 @Async
注解。
大家猜猜程序启动后运行后果会怎么?
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] 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.
报错了。。。起因是呈现了循环依赖。
「不迷信呀,spring 不是号称能解决循环依赖问题吗,怎么还会呈现?」
如果把下面的代码略微调整一下:
@Service
publicclass TestService1 {
@Autowired
private TestService2 testService2;
public void test1() {}
}
把 TestService1 的 test1 办法上的 @Async 注解去掉,TestService1 和 TestService2 都须要注入对方的实例,同样形成了循环依赖。
然而重新启动我的项目,发现它可能失常运行。这又是为什么?
带着这两个问题,让咱们一起开始 spring 循环依赖的探秘之旅。
2. 什么是循环依赖?
循环依赖:说白是一个或多个对象实例之间存在间接或间接的依赖关系,这种依赖关系形成了形成一个环形调用。
第一种状况:本人依赖本人的间接依赖
第二种状况:两个对象之间的间接依赖
第三种状况:多个对象之间的间接依赖
后面两种状况的间接循环依赖比拟直观,十分好辨认,然而第三种间接循环依赖的状况有时候因为业务代码调用层级很深,不容易辨认进去。
3. 循环依赖的 N 种场景
spring 中呈现循环依赖次要有以下场景:
单例的 setter 注入
这种注入形式应该是 spring 用的最多的,代码如下:
@Service
publicclass TestService1 {
@Autowired
private TestService2 testService2;
public void test1() {}
}
@Service
publicclass TestService2 {
@Autowired
private TestService1 testService1;
public void test2() {}
}
这是一个经典的循环依赖,然而它能失常运行,得益于 spring 的外部机制,让咱们根本无法感知它有问题,因为 spring 默默帮咱们解决了。
spring 外部有三级缓存:
- singletonObjects 一级缓存,用于保留实例化、注入、初始化实现的 bean 实例
- earlySingletonObjects 二级缓存,用于保留实例化实现的 bean 实例
- singletonFactories 三级缓存,用于保留 bean 创立工厂,以便于前面扩大有机会创立代理对象。
上面用一张图通知你,spring 是如何解决循环依赖的:
图 1
仔细的敌人可能会发现在这种场景中第二级缓存作用不大。
那么问题来了,为什么要用第二级缓存呢?
试想一下,如果呈现以下这种状况,咱们要如何解决?
@Service
publicclass TestService1 {
@Autowired
private TestService2 testService2;
@Autowired
private TestService3 testService3;
public void test1() {}
}
@Service
publicclass TestService2 {
@Autowired
private TestService1 testService1;
public void test2() {}
}
@Service
publicclass TestService3 {
@Autowired
private TestService1 testService1;
public void test3() {}
}
TestService1 依赖于 TestService2 和 TestService3,而 TestService2 依赖于 TestService1,同时 TestService3 也依赖于 TestService1。
依照上图的流程能够把 TestService1 注入到 TestService2,并且 TestService1 的实例是从第三级缓存中获取的。
假如不必第二级缓存,TestService1 注入到 TestService3 的流程如图:
图 2
TestService1 注入到 TestService3 又须要从第三级缓存中获取实例,而第三级缓存里保留的并非真正的实例对象,而是 ObjectFactory 对象。说白了,两次从三级缓存中获取都是 ObjectFactory 对象,而通过它创立的实例对象每次可能都不一样的。
这样不是有问题?
为了解决这个问题,spring 引入的第二级缓存。下面图 1 其实 TestService1 对象的实例曾经被增加到第二级缓存中了,而在 TestService1 注入到 TestService3 时,只用从第二级缓存中获取该对象即可。
图 3
还有个问题,第三级缓存中为什么要增加 ObjectFactory 对象,间接保留实例对象不行吗?
答:不行,因为如果你想对增加到三级缓存中的实例对象进行加强,间接用实例对象是行不通的。
针对这种场景 spring 是怎么做的呢?
答案就在 AbstractAutowireCapableBeanFactory 类 doCreateBean 办法的这段代码中:
它定义了一个匿名外部类,通过 getEarlyBeanReference 办法获取代理对象,其实底层是通过 AbstractAutoProxyCreator 类的 getEarlyBeanReference 生成代理对象。
多例的 setter 注入
这种注入办法偶尔会有,特地是在多线程的场景下,具体代码如下:
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService1 {
@Autowired
private TestService2 testService2;
public void test1() {}
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService2 {
@Autowired
private TestService1 testService1;
public void test2() {}
}
很多人说这种状况 spring 容器启动会报错,其实是不对的,我十分负责任的通知你程序可能失常启动。
为什么呢?
其实在 AbstractApplicationContext 类的 refresh 办法中通知了咱们答案,它会调用 finishBeanFactoryInitialization 办法,该办法的作用是为了 spring 容器启动的时候提前初始化一些 bean。该办法的外部又调用了 preInstantiateSingletons 办法
标红的中央显著可能看出:非形象、单例 并且非懒加载的类能力被提前初始 bean。
而多例即 SCOPE_PROTOTYPE 类型的类,非单例,不会被提前初始化 bean,所以程序可能失常启动。
如何让他提前初始化 bean 呢?
只须要再定义一个单例的类,在它外面注入 TestService1
@Service
publicclass TestService3 {
@Autowired
private TestService1 testService1;
}
重新启动程序,执行后果:
Requested bean is currently in creation: Is there an unresolvable circular reference?
果然呈现了循环依赖。
留神:这种循环依赖问题是无奈解决的,因为它没有用缓存,每次都会生成一个新对象。
结构器注入
这种注入形式是 spring4.x 以上的版本中官网举荐的形式,具体如下代码:
@Service
publicclass TestService1 {public TestService1(TestService2 testService2) {}}
@Service
publicclass TestService2 {public TestService2(TestService1 testService1) {}}
运行后果:
Requested bean is currently in creation: Is there an unresolvable circular reference?
呈现了循环依赖,为什么呢?
从图中的流程看出结构器注入只是增加了三级缓存,并没有应用缓存,所以也无奈解决循环依赖问题。
单例的代理对象 setter 注入
这种注入形式其实也比拟罕用,比方平时应用:@Async
注解的场景,会通过 AOP 主动生成代理对象。
我那位共事的问题也是这种状况。
@Service
publicclass TestService1 {
@Autowired
private TestService2 testService2;
@Async
public void test1() {}
}
@Service
publicclass TestService2 {
@Autowired
private TestService1 testService1;
public void test2() {}
}
从后面得悉程序启动会报错,呈现了循环依赖:
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] 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.
为什么会循环依赖呢?
答案就在上面这张图中:
说白了,bean 初始化实现之后,前面还有一步去查看:第二级缓存 和 原始对象 是否相等。因为它对后面流程来说无关紧要,所以后面的流程图中省略了,然而在这里是关键点,咱们重点说说:
那位共事的问题正好是走到这段代码,发现第二级缓存 和 原始对象不相等,所以抛出了循环依赖的异样。
如果这时候把 TestService1 改个名字,改成:TestService6,其余的都不变。
@Service
publicclass TestService6 {
@Autowired
private TestService2 testService2;
@Async
public void test1() {}
}
再重新启动一下程序,神奇般的好了。
what?这又是为什么?
这就要从 spring 的 bean 加载程序说起了,默认状况下,spring 是依照文件残缺门路递归查找的,按门路 + 文件名排序,排在后面的先加载。所以 TestService1 比 TestService2 先加载,而改了文件名称之后,TestService2 比 TestService6 先加载。
为什么 TestService2 比 TestService6 先加载就没问题呢?
答案在上面这张图中:
这种状况 testService6 中其实第二级缓存是空的,不须要跟原始对象判断,所以不会抛出循环依赖。
DependsOn 循环依赖
还有一种有些非凡的场景,比方咱们须要在实例化 Bean A 之前,先实例化 Bean B,这个时候就能够应用 @DependsOn 注解。
@DependsOn(value = "testService2")
@Service
publicclass TestService1 {
@Autowired
private TestService2 testService2;
public void test1() {}
}
@DependsOn(value = "testService1")
@Service
publicclass TestService2 {
@Autowired
private TestService1 testService1;
public void test2() {}
}
程序启动之后,执行后果:
Circular depends-on relationship between 'testService2' and 'testService1'
这个例子中原本如果 TestService1 和 TestService2 都没有加 @DependsOn 注解是没问题的,反而加了这个注解会呈现循环依赖问题。
这又是为什么?
答案在 AbstractBeanFactory 类的 doGetBean 办法的这段代码中:
它会查看 dependsOn 的实例有没有循环依赖,如果有循环依赖则抛异样。
4. 呈现循环依赖如何解决?
我的项目中如果呈现循环依赖问题,阐明是 spring 默认无奈解决的循环依赖,要看我的项目的打印日志,属于哪种循环依赖。目前蕴含上面几种状况:
生成代理对象产生的循环依赖
这类循环依赖问题解决办法很多,次要有:
- 应用 @Lazy 注解,提早加载
- 应用 @DependsOn 注解,指定加载先后关系
- 批改文件名称,扭转循环依赖类的加载程序
应用 @DependsOn 产生的循环依赖
这类循环依赖问题要找到 @DependsOn 注解循环依赖的中央,迫使它不循环依赖就能够解决问题。
多例循环依赖
这类循环依赖问题能够通过把 bean 改成单例的解决。
结构器循环依赖
这类循环依赖问题能够通过应用 @Lazy 注解解决。
当然最好的解决循环依赖问题最佳计划是从代码设计上躲避,然而简单的零碎中有可能没法防止。
最初说一句(求关注,别白嫖我)
如果这篇文章对您有所帮忙,或者有所启发的话,帮忙扫描下发二维码关注一下,您的反对是我保持写作最大的能源。
求一键三连:点赞、转发、在看。
在公众号中回复:面试、代码神器、开发手册、工夫治理有超赞的粉丝福利,另外回复:加群,能够跟很多 BAT 大厂的前辈交换和学习。
本文由博客群发一文多发等经营工具平台 OpenWrite 公布