面试题24:Spring循环依赖
Spring循环依赖:原理、问题与解决方案
一、循环依赖的定义
在Spring框架中,循环依赖是指两个或多个Bean之间相互依赖,形成一个闭环的情况。例如,BeanA依赖于BeanB,同时BeanB又依赖于BeanA。这种情况在通过setter
方法注入或者@Autowired
属性注入时较为常见。
代码示例
// BeanA类
public class BeanA {private BeanB beanB;// setter方法注入BeanBpublic void setBeanB(BeanB beanB) {this.beanB = beanB;}
}// BeanB类
public class BeanB {private BeanA beanA;// setter方法注入BeanApublic 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注解@Autowiredpublic 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的正常初始化顺序。