Spring循环依赖:原理、问题与解决方案
一、循环依赖的定义
在Spring框架中,循环依赖是指两个或多个Bean之间相互依赖,形成一个闭环的情况。例如,BeanA依赖于BeanB,同时BeanB又依赖于BeanA。这种情况在通过setter
方法注入或者@Autowired
属性注入时较为常见。
代码示例
// BeanA类
public class BeanA {
private BeanB beanB;
// setter方法注入BeanB
public void setBeanB(BeanB beanB) {
this.beanB = beanB;
}
}
// BeanB类
public class BeanB {
private BeanA beanA;
// setter方法注入BeanA
public void setBeanA(BeanA beanA) {
this.beanA = beanA;
}
}
二、Spring三级缓存机制
(一)缓存结构
-
一级缓存
-
作用:存放完全初始化好的Bean实例。
-
存储类型:
ConcurrentHashMap
类型的SingletonObjects
。 -
意义:当需要获取一个已经完全初始化的Bean时,可以直接从一级缓存中获取,提高获取Bean的效率。
-
-
二级缓存
-
作用:存放未完成属性注入的Bean实例。
-
存储类型:
HashMap
类型的earlySingletonObjects
。 -
意义:在Bean实例创建过程中,当实例已经创建但还未完成属性注入等操作时,先将这个“半成品”的Bean实例存放在二级缓存中,以应对可能出现的其他Bean对它的依赖。
-
-
三级缓存
-
作用:存放Bean的
ObjectFactory
。 -
存储类型:
HashMap
类型的singletonFactories
。 -
意义:
ObjectFactory
是一个用于懒加载Bean的接口,它可以在需要时提供Bean的实例,而不是立即创建和存储它。这一机制有助于解决setter
方法和@Autowired
属性注入的循环依赖问题。
-
(二)Bean创建流程与三级缓存的关系
-
当创建一个Bean(例如BeanA)时,首先会将BeanA的
ObjectFactory
放入三级缓存。 -
在BeanA实例创建完成后,会进行属性注入。如果在注入属性时发现需要BeanB,那么就会开始创建BeanB。
-
同样,创建BeanB时,也会将BeanB的
ObjectFactory
放入三级缓存。 -
在注入BeanB的属性时,如果发现需要BeanA,此时就可以从三级缓存中获取到BeanA的
ObjectFactory
,并通过它来获取BeanA的实例(这个实例可能是还未完全初始化的),然后将这个实例注入到BeanB中,之后再将这个实例放入二级缓存,继续完成BeanB的其他初始化操作。最后,将完全初始化后的BeanB实例放入一级缓存,再将BeanB注入到BeanA中,从而完成BeanA的初始化。
三、构造器注入的循环依赖问题
(一)问题产生的原因
-
三级缓存机制只有在对象实例已经创建(即构造方法执行完后)才能生效,从而将实例暴露到
earlySingletonObjects
中。 -
在使用构造器注入时,Spring必须先创建依赖对象才能调用构造器。例如,BeanA的构造器必须要BeanB的实例作为参数,而BeanB又等着BeanA的实例,这就导致Spring在创建BeanA时,由于需要BeanB的实例,但BeanB的创建又依赖于BeanA的实例,使得Spring根本无法创建任何一个Bean的实例,从而导致了死锁。
(二)解决方案:@Lazy注解
-
原理
-
可以通过在构造器中对参数添加
@Lazy
注解,让Spring注入一个代理对象。这个代理对象并不会立即触发依赖Bean的真正初始化,而是在真正调用该依赖Bean的方法时才会触发其初始化。
-
-
示例代码修改
-
对于BeanA的构造器进行修改:
public class BeanA { private BeanB beanB; // 使用@Lazy注解 @Autowired public BeanA(@Lazy BeanB beanB) { this.beanB = beanB; } }
-
四、其他相关问题与最佳实践
(一)其他解决方案探讨
-
@PostConstruct注解
-
虽然不能直接解决构造器注入的循环依赖问题,但可以在Bean的初始化阶段进行一些操作,例如在
@PostConstruct
注解标注的方法中进行一些属性的设置或者资源的加载,这些操作可以在依赖注入完成之后进行。
-
-
使用setter注入
-
在可能的情况下,推荐使用setter注入而非构造器注入。因为setter注入不会在实例创建时就需要依赖的Bean,而是在Bean实例创建完成后再进行属性注入,这样可以避免构造器注入带来的循环依赖问题。
-
(二)Bean作用域对循环依赖的影响
-
对于
prototype
作用域的Bean,由于其每次获取时都会创建新的实例,所以在处理循环依赖时相对复杂。如果出现循环依赖,可能会导致更多的问题,因为无法像单例Bean那样可以通过缓存机制来解决。
(三)最佳实践总结
-
在设计Bean之间的依赖关系时,尽量避免循环依赖的出现。如果无法避免,优先考虑使用setter注入或者字段注入(通过
@Autowired
)的方式。 -
如果必须使用构造器注入且出现了循环依赖问题,可以考虑使用
@Lazy
注解来解决。 -
在使用
@PostConstruct
注解时,要确保其标注的方法中的操作不会引入新的循环依赖或者影响Bean的正常初始化顺序。