循环引用
每日一句诗词
loading...
loading...
问题的产生
- 代码出现两个bean在创建时都相互引用了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class A {
private B b;
public A(B b){
this.b = b;
}
}
public class B {
private A a;
public B(A a){
this.a = a;
}
}
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Springboot.class, args);
Object a = run.getBean("a");
}
我们在向容器获取A对象是出错了
1
2
3
4
5
6
7
8
9
10
11
12The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| a defined in file [D:\download\SpringBoot\springboot_demo05\target\classes\com\panther\springboot\mycompoent\A.class]
↑ ↓
| b defined in file [D:\download\SpringBoot\springboot_demo05\target\classes\com\panther\springboot\mycompoent\B.class]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.解决办法
方案一
自己创建对象,不使用IOC自动注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55public class A {
private B b;
public A(){
}
public A(B b){
this.b = b;
}
public void setB(B b) {
this.b = b;
}
public B getB() {
return b;
}
}
public class B {
private A a;
public B(){
}
public B(A a){
this.a = a;
}
public void setA(A a) {
this.a = a;
}
public A getA() {
return a;
}
}
private static final Logger logger = Logger.getLogger("logger");
public static void main(String[] args) {
SpringApplication.run(Springboot.class, args);
// Object a = run.getBean("a");
A a = new A();
B b = new B();
b.setA(a);
a.setB(b);
logger.info(String.valueOf(a.getB() == b)); //true
logger.info(String.valueOf(b.getA() == a)); //true
logger.info(String.valueOf(a.getB()); //com.panther.test@B5947
logger.info(String.valueOf(b.getA()); //com.panther.test@A5945
}DEBUG分析
发现在
new
a和b时 属性都是空的- 向下继续运行时,好像还是产生了循环依赖,但为什么a.getB()能输出B的实体类
- 有点懵逼。。。
百度的拼凑版本解释: jvm在
new
一个对象时,并不会创建这个对象,只是向内存申请了一段空间,属于半初始化的实体类。方案二
Spring框架提供的解决办法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class A {
private B b;
public void setB(B b) {
this.b = b;
}
}
public class B {
private A a;
public void setA(A a) {
this.a = a;
}
}
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Springboot.class, args);
A a = run.getBean(A.class);
B b = run.getBean(B.class);
logger.info(String.valueOf(a.getB() == b)); //true
logger.info(String.valueOf(b.getA() == a)); //true
logger.info(b.getA());//com.panther.springboot.test.A@27e7c77f
logger.info(a.getB());//com.panther.springboot.test.B@6f70a21b
}
- 开启循环引用配置
1
spring.main.allow-circular-references: true
- spring 引入三级缓存去解决循环引用
一级缓存 singletonObjects: 主要存放的是已经完成实例化、属性填充和初始化所有步骤的单例Bean实例,这样的Bean能够直接提供给用户使用,我们称之为终态Bean或叫成熟Bean。
二级缓存 earlySingletonObjects: 主要存放的已经完成初始化但属性还没自动赋值的Bean,这些Bean还不能提供用户使用,只是用于提前暴露的Bean实例,我们把这样的Bean称之为临时Bean或早期的Bean(半成品Bean)
- 三级缓存 singletonFactories: 存放的是ObjectFactory的匿名内部类实例,调用ObjectFactory.getObject()最终会调用getEarlyBeanReference方法,该方法可以获取提前暴露的单例bean引用。
过程1
2
3
4
5
6
7
8//一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 三级缓存 map中存放早期半初始化的的bean,并提前暴露
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
//二级缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
我们假设现在有这样的场景AService依赖BService,BService依赖AService
- 一开始加载AService Bean首先依次从一二三级缓存中查找是否存在beanName=AService的对象。
1
2
3 final String beanName = transformedBeanName(name);
// 1.尝试从缓存中获取bean,AService还没创建三级缓存都没命中
Object sharedInstance = getSingleton(beanName);
- 三级缓存都没命中于是走到创建Bean代码逻辑
1
2
3
4
5
6
7 //将当前beanName放到singletonsCurrentlyInCreation 集合中,标识该bean正在创建
beforeSingletonCreation(beanName);
//通过回调getObject()方法触发AbstractAutowireCapableBeanFactory#createBean(String beanName, RootBeanDefinition mbd, Object[] args)的执行
singletonObject = singletonFactory.getObject();
afterSingletonCreation(beanName);
// 此时Key为AService,value是ObjectFactory类型一个匿名内部类对象放入三级缓存中
addSingleton(beanName, singletonObject);
- 继续对AService进行属性填充(依赖注入),这时发现AService依赖BService。于是又依次从一二三级缓存中查询BService Bean,没找到,于是又按照上述的流程实例化BService,将以Key为BService,value是ObjectFactory类型一个匿名内部类对象放入三级缓存中。
- 这时发现BService又依赖AService。于是依次在一二三级缓存中查找AService。
- 在三级缓存中查到之前放入的以 Key 为 AService,如果没AOP切面对 AService 进行拦截,这时返回的将是AService实例本身,将半成品 AService Bean放入二级缓存并将 Key 为AService从三级缓存中删除。 这样实现了提前将 AService Bean 曝光给BService完成属性依赖注入。继续走BService后续初始化逻辑,最后生产了成熟的BService Bean实例。
- 接着原路返回,AService也成功获取到依赖BService实例,完成后续的初始化工作,然后完美的解决了循环依赖的问题。
完整流程图
- 如果A实现类AOP增强可以发现bservice实体类里的A属性是一个代理对象
1
2
3
4
5
6
7if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}方案三
自己写一个代理去解决
1
2
3
4
5
6
7
8
9
public class MyAspectJ {
public void before(){
System.out.println("---------");
}
}
为什么只支持单例
Spring循环依赖的解决方案主要是通过对象的提前暴露来实现的。当一个对象在创建过程中需要引用到另一个正在创建的对象时,Spring会先提前暴露一个尚未完全初始化的对象实例,以解决循环依赖的问题。这个尚未完全初始化的对象实例就是半成品对象。
在 Spring 容器中,单例对象的创建和初始化只会发生一次,并且在容器启动时就完成了。这意味着,在容器运行期间,单例对象的依赖关系不会发生变化。因此,可以通过提前暴露半成品对象的方式来解决循环依赖的问题。
相比之下,原型对象的创建和初始化可以发生多次,并且可能在容器运行期间动态地发生变化。因此,对于原型对象,提前暴露半成品对象并不能解决循环依赖的问题,因为在后续的创建过程中,可能会涉及到不同的原型对象实例,无法像单例对象那样缓存并复用半成品对象。
因此,Spring只支持通过单例对象的提前暴露来解决循环依赖问题。
为什么不支持构造函数注入
Spring无法解决构造函数的循环依赖,是因为在对象实例化过程中,构造函数是最先被调用的,而此时对象还未完成实例化,无法注入一个尚未完全创建的对象,因此Spring容器无法在构造函数注入中实现循环依赖的解决。
1 |
|
在属性注入中,Spring容器可以通过先创建一个空对象或者提前暴露一个半成品对象来解决循环依赖的问题。但在构造函数注入中,对象的实例化是在构造函数中完成的,这样就无法使用类似的方式解决循环依赖问题了。
如何解决构造器注入的循环依赖
构造器注入的循环依赖,可以通过一定的手段解决。
1、重新设计,彻底消除循环依赖
循环依赖,一般都是设计不合理导致的,可以从根本上做一些重构,来彻底解决,
2、改非构造器注入
可以改成setter注入或者字段注入。
3、使用@Lazy解决