问题表现
代码上线,发现启动报错
![](https://i-blog.csdnimg.cn/blog_migrate/941219ed50658804d75720e58930c404.png)
很明显发生了循环依赖。
上线人员表示,我就是按照之前的代码结构加的,之前代码没有问题,现在怎么就有问题了呢。
我们看下代码
代码
我将这个问题,做一个demo,用来描述这个问题
demo整体结构
![](https://i-blog.csdnimg.cn/blog_migrate/c33870d761a44725121e6b1784111315.png)
很简单,一个启动类,三个bean。用来复现问题
具体代码
Config
public class Config {
public void say() {
System.out.println("do say");
}
}
TestService
@Service
public class TestService {
@Autowired
private Config config;
@PostConstruct
private void init() {
System.out.println("this is BeanTest init function. ");
config.say();
}
@Bean
public Config config(){
System.out.println("new Config");
return new Config();
}
}
TService
@Service
public class TService {
@Autowired
private Config config;
}
上面就是demo 的代码,他基本描述了真实项目里的问题。
问题分析
原因
这里主要是三个bean ,TService ,TestService 和Config,他们之间的关系如下所示:
如果我们debug ,会发现最终会直接抛出了BeanCurrentlyInCreationException 这个异常。
这个初始化的流程是:TService 初始化的时候,里面注入了Config ,因此需要先初始化Config。Config 在TestService 这个bean 里被@bean 注解修饰,因此要想初始化成功Config ,就需要初始化出TestService,当初始化
TestService的时候,发现Config 被注入了,因此有需要初始化Config ,这样就形成了一个环,抛出异常。
为什么别的地方没问题
根据上面的结论来看,如果项目初始化TestService 这个bean ,就会发现没有问题。查资料,bean 初始化的顺序为名字的字母排序,因此如果把TService 改成ZService ,在这里理论上就不会有问题。
![](https://i-blog.csdnimg.cn/blog_migrate/ba145d5ca9292d9005933a0c07389e52.png)
![](https://i-blog.csdnimg.cn/blog_migrate/de49f36c8fe3eecffe729f1fb2853545.png)
事实证明 确实如此。
解决办法
这个问题出现的并不高级,解决办法非常多,比如调整bean 的加载逻辑,Config 移出来,等等
问题延伸
三种循环依赖的场景
构造器的循环依赖:这种依赖spring是处理不了的,直接抛出BeanCurrentlylnCreationException异常。
单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖,能处理。
非单例循环依赖:无法处理。原型(Prototype)的场景是不支持循环依赖的,通常会走到AbstractBeanFactory类中下面的判断,抛出异常。
Spring 三级缓存解决循环依赖
本质原理
java中只有值传递,但是这里可以借用一个引用传递的概念,这个引用传递指的是,当获得对象的引用时,对象的属性是可以延后设置的。(但是构造器必须是在获取引用之前,毕竟引用是靠构造器给你生成的)
具体分析
Spring创建Bean的流程
分析这个问题,我们先来看看bean的整个创建流程
![](https://i-blog.csdnimg.cn/blog_migrate/c9f55923b2baed88d398bee18a12a8f5.png)
对Bean的创建最为核心三个方法解释如下:
createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象
populateBean:填充属性,这一步主要是对bean的依赖属性进行注入(@Autowired)
initializeBean:回到一些形如initMethod、InitializingBean等方法
从对单例Bean的初始化可以看出,循环依赖主要发生在第二步(populateBean),也就是field属性注入的处理。
处理方案
Spring内部维护了三个Map,也就是我们通常说的三级缓存。在Spring的DefaultSingletonBeanRegistry类中,会赫然发现类上方挂着这三个Map
![](https://i-blog.csdnimg.cn/blog_migrate/92d2de2c2b6a6e08c46ccc3389652719.png)
singletonObjects : (一级缓存)“单例池”“容器”,缓存创建完成单例Bean的地方。
earlySingletonObjects:(二级缓存)映射Bean的早期引用,也就是说在这个Map里的Bean不是完整的,甚至还不能称之为“Bean”,只是一个Instance,用于解决循环依赖
singletonFactories:(三级缓存) 映射创建Bean的原始工厂,用于解决循环依赖
当 Spring 为某个 Bean 填充属性的时候,它首先会寻找需要注入对象的名称,然后依次执行 getSingleton() 方法得到所需注入的对象,而获取对象的过程就是先从一级缓存中获取,一级缓存中没有就从二级缓存中获取,二级缓存中没有就从三级缓存中获取,如果三级缓存中也没有,那么就会去执行 doCreateBean() 方法创建这个 Bean。
Spring 是如何通过这三级缓存来解决循环依赖的呢?
我们举一个简单的例子,把上面的Config 和 TService变一下
@Service
public class Config {
@Autowired
private TService tService;
public void say() {
System.out.println("do say");
}
}
@Service
public class TService {
@Autowired
private Config config;
}
发现可以正常启动
![](https://i-blog.csdnimg.cn/blog_migrate/8742a856361cc1d7ea73a3a65d414592.png)
这里的加载流程是
实例化 TService 还未完成属性填充和初始化方法(@PostConstruct)的执行,TService 只是一个半成品。
为 TService 创建一个 Bean工厂,并放入到 singletonFactories 中。
发现 TService需要注入Config 对象,但是一级、二级、三级缓存均未发现对象 Config。
实例化Config,此时 Config 还未完成属性填充和初始化方法(@PostConstruct)的执行,Config 只是一个半成品。
为 Config 创建一个 Bean工厂,并放入到 singletonFactories 中。
发现 Config 需要注入 TService 对象,此时在一级、二级未发现对象TService,但是在三级缓存中发现了对象 TService,从三级缓存中得到对象 TService,并将对象 TService 放入二级缓存中,同时删除三级缓存中的对象TService。(注意,此时的TService还是一个半成品,并没有完成属性填充和执行初始化方法)
将对象 TService 注入到对象Config 中。对象Config 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 Config。(此时对象 Config 已经是一个成品)
对象 TService得到对象Config,将对象 Config 注入到对象 TService 中。(对象 TService 得到的是一个完整的对象 Config)
对象 TService完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 TService。