Spring是如何解决循环依赖的呢?
很多小伙伴在面试时都被问到过这个问题,刷到过这个题的同学马上就能回答出来:“利用三级缓存”。面试官接着追问:“哪三级缓存呢?用两级行不行呢?” 这时候如果没有深入研究过Spring源码的话,估计就要根据自己的理解开始胡诌了。为了帮大家彻底理清这个问题,吊打面试官,我特意出这篇文章来帮你梳理一下Spring的解决思路。
解决循环依赖的前提是创建的Bean是都单例的,本章的所有Bean默认都采用单例模式。
我假定屏幕前的你已经熟练使用Spring并且知道IOC的原理了。
Spring 的 BeanFactory 在创建 Bean 时会经历三个重要步骤
- 实例化bean (createBeanInstance)
- 给bean的属性赋值 (populateBean)
- 初始化bean (initializeBean)
每个Bean在被创建时都需要按部就班走完这三步后,才能诞生一个可用的Bean。
先看一个没有循环依赖的例子
假如一个Bean(AService)持有另一个Bean(BService) 的引用,两个Bean是怎么个创建步骤呢?
AService与BService的类定义如下
@Service
public class AService {
@Autowired
BService bService;
public void seeA() {
bService.eat();
}
}
@Service
public class BService {
public void eat() {
System.out.println("B======eat");
}
}
AService持有一个BService的引用,如果在创建AService这个Bean时,BService还没创建的话,那么BService也会被创建。流程如下图
可以看到这两个Bean的创建流程很明了。
Bean创建完成后会放到一个单例池中,这个单例池就是三级缓存中的“一级缓存”。
Spring用一个Map来充当这个单例池,key是beanName,value为Bean对象。
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
构建一个循环依赖(不考虑AOP代理逻辑)
假如BService中又引用了AService,两个Bean各自依赖对方,形成了循环依赖,如果还沿用上面的流程来创建Bean的话,就会陷入到一个无止境的循环中。
现在AService和BService的代码如下
@Service
public class AService {
@Autowired
BService bService;
public void seeA() {
bService.eat();
}
public void eat() {
System.out.println("A======eat");
}
}
@Service
public class BService {
@Autowired
AService aService;
public void seeB() {
aService.eat();
}
public void eat() {
System.out.println("B======eat");
}
}
怎么打破这个循环呢?
Spring是这样想的:创建一个Bean需要经过三个步骤,在第一步实例化Bean这个操作完成后,这个Bean对象在内存中就已经存在了,就能拿到该对象的引用了,但是这个对象还是个半成品,之后的第二、三步都是在丰富这个对象,第三步走完后这个对象才是个成品对象。所以第一步走完后,就可以找个地方将该Bean的引用存放起来,因为还是个半成品,所以不能放到“一级缓存”中, Spring就为它创建了一个新地方,这个地方就是“二级缓存”,Spring用一个Map来表示二级缓存,key是beanName,value为Bean半成品对象。
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
拿上面的例子来说,AService在被实例化后,Spring就将AService的引用放到二级缓存中,然后第二步填充属性时触发了BService的Bean创建流程,BService在第二步填充属性时发现又依赖了AService,于是尝试获取AService的Bean实例,一级缓存中没有,二级缓存中有,于是就将二级缓存中的“AService引用”拿过来填充属性值。这样循环就被打破了。
来画图说明一下
我用一个2×2的表格,来代表一级和二级缓存
两个Bean的创建流程如下图,我将两级缓存的内容放在时序图的左侧,你可以清楚的看到,两级缓存的内容是在什么时候发生变化的。
可以看到,如果只是简简单单的两个Bean相互依赖的话,引入一个存放半成品Bean的二级缓存就可以打破这个循环依赖了。
构建一个循环依赖(考虑AOP代理逻辑)
但是如果产生循环依赖的Bean同时又被AOP代理,需要产生代理对象的话,两级缓存就显得有点不够用了。
默认情况下,Spring将AOP产生代理对象的逻辑放在了“初始化Bean”这一步里,并且是放在了执行init方法之后,通过一段简略代码来看一下初始化Bean的流程
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
// 如果Bean实现了Aware接口,需要执行对应Aware的逻辑
invokeAwareMethods(beanName, bean);
Object wrappedBean = bean;
// 执行Bean初始化前需要执行的前置操作
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean,
// 执行init方法,初始化bean
invokeInitMethods(beanName, wrappedBean, mbd);
// 执行Bean初始化后需要执行的后置操作,代理对象就是在这一步生成
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean,
return wrappedBean;
}
所以,Bean创建三步曲中,第一步实例化的Bean可能不是最终的Bean,如果这个Bean有代理逻辑的话,在第三步初始化Bean后,就产生了一个代理Bean,最终放入单例池中的也是这个代理Bean。
如何非要还用两级代理的话,能实现吗?
我觉得也能实现,但是要将AOP的代理逻辑往前提,提到第一步中,在实例化Bean之后,马上就执行代理逻辑,并且放入二级缓存里的也是代理后的对象。这样的话当循环依赖产生时,从二级缓存中也能直接获取到最终的代理对象。
但是这个做法貌似违背了Spring的设计初衷,Spring是将AOP的代理逻辑放在偏后的位置的,前面也说了,是放在执行了Bean对象的init方法后的。也就是说Spring的设计是:先将一个完整的Bean对象创建好,然后再看需不需要进行AOP代理。
那么Spring最终是怎么做的呢?Spring没有修改原来的设计,而是又加了一级缓存,也就是“三级缓存”,这第三级缓存就是用来处理AOP代理逻辑的。Spring允许当某个Bean产生循环依赖时提前执行其AOP代理逻辑,但是当没有循环依赖产生时,仍然是按部就班的先创建好Bean后,再尝试进行AOP代理。
Spring用一个Map来表示三级缓存,key是beanName,value为ObjectFactory对象。
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
这个ObjectFactory是何方神圣呢?看代码
它是一个接口,并且只有一个方法 getObject() ,这个接口上有一个@FunctionalInterface注解,这个注解用于指示一个接口是一个函数式接口。函数式接口是指仅包含一个抽象方法的接口,这样的接口可以用作 Lambda 表达式或方法引用的目标。而Spring也是采用了Lambda表达式的方式来使用这个接口。
来看两段源码
Spring往三级缓存中加对象的方法是这样定义的
调用 addSingletonFactory 这个方法的方式是这样的
可以看到调用处是用了一个Lambda表达式来表示 ObjectFactory 这个接口的具体实现。而getEarlyBeanReference这个方法则是代表了 ObjectFactory 接口中的 getObject() 方法。三级缓存就是调用这个方法来拿到一个半成品Bean的。
继续我们的代码和流程演示
这里我为上面的AService和BService加了一个AOP代理类
@Aspect
@Component
@Slf4j
public class MyAdvice {
@Pointcut("execution(* com.sundries.circular_dependency.*Service.*(..))")
public void pointCut(){}
@Around("pointCut()")
public Object aroundDeal(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object result = joinPoint.proceed();
stopWatch.stop();
long totalTimeMillis = stopWatch.getTotalTimeMillis();
log.info("{} 方法执行耗时: {}ms", methodName, totalTimeMillis);
return result;
}
}
这个类里定义的pointCut就是指定了要切AService合BService两个类的所有方法,代理逻辑很简单,就是加了一个方法执行耗时的打印。
下面来看看使用三级缓存时,两个Bean的创建流程图。注意!这个流程已经是Spring的真实逻辑了。
Spring就是这样设计了一个三级缓存来解决了Bean的循环依赖问题。解决这个问题的核心思想就是提前将一个半成品的Bean引用给暴露出来,提供给其他Bean来作为属性值。
文末了,如果这里再问你一下:Spring是如何解决循环依赖的呢? 相信你已经能随口说出其中设计原理了👍🏻
下面是我根据Spring源码梳理这个流程时的过程图,感兴趣的话可以放大看下
不存在AOP代理时的情况
存在AOP代理时的情况