一步一步深入spring源码彻底搞懂循环依赖问题

前言

spring循环依赖相信不少准备面试或者正在面试的小伙伴都会或多或少的都会碰到spring循环依赖的问题,而说到循环依赖的解决方案,稍微看过点面试题的都会想到几个关键词:三级缓存提前暴露对象等。但是spring内部到底是怎么解决的呢?为什么要用三级缓存,二级缓存不行吗?本文会通过一步步分析和阅读源码好好盘一盘这个spring循环依赖!!

简介

spring循环依赖主要有构造器setter两种形式。构造器的循环依赖spring默认是不给我们解决的,直接抛出BeanCurrentlyInCreationException异常。这个比较简单,稍微分析一下:
在这里插入图片描述
比如我有两个类,A依赖B,B依赖A,当spring调用A的构造函数时,会将A标记为正在创建中,A依赖B,检测到容器中没有B对象,spring就回去创建B对象,当spring调用B的构造函数时,会将B标记为正在创建中,B依赖A,而此时A仅仅是正在创建中,容器中还没有A对象,所以spring又会去创建A,从而会抛出BeanCurrentlyInCreationException异常。
setter循环依赖就是通过属性值注入,比较常见的就是我们经常使用的@Autowired注解。这种形式的循环依赖spring为我们给出的解决方案是三级缓存,下面我们一点点的来分析一下循环依赖是如何形成的,以及解决它的基本思路,最后再通过源码进行验证。

分析

三级缓存

在分析之前我们先来简单了解一下spring的三级缓存到底长什么样子,定义在哪,先混个眼熟~~

public class DefaultSingletonBeanRegistry ... {
	...
	//一级缓存
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	//三级缓存
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	//二级缓存
	public final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
	...
}

三级缓存定义在DefaultSingletonBeanRegistry类中,我们常说的IOC容器一般指的就是这个一级缓存,单例Bean在创建完成之后都会存放在singletonObjects中,所以不管是否存在循环依赖,这个一级缓存是必须要存在的。二级缓存和三级缓存我们这里先不做详细解释,在后续的分析中都会讲到。

spring bean生命周期

spring bean的生命周期不是本文的重点,但是讲循环依赖总是避不开spring bean的生命周期,我这里就不做过多的文字描述,感兴趣的话可以去看一下spring源码中BeanFactory这个类上面的注释,对于bean的生命周期描述的还是比较详细的,我这里就直接贴一张图吧:
在这里插入图片描述
通过上面这张图我想强调的是,spring在创建对象的过程中实例化和初始化是分开的!!实例化仅仅是开辟一块内存区域,实例化完成之后对象中的属性都是默认值状态;而初始化才会给对象中的属性赋值。这一点对于我们理解spring循环依赖很重要。

循环依赖

循环依赖其实就是循环引用,也就是两个或则两个以上的 Bean 互相持有对方,最终形成闭环。比如A类和B类,代码如下:

@Service
public class A {
	@Auowired
	B b;
}

@Service
public class B {
	@Auowired
	A a;
}

如果不考虑任何解决方案,对象的创建流程如下图:
在这里插入图片描述
当A对象初始化去容器中找B对象的时候,B对象是不存在的;所以接下来就会创建B对象,当给B对象初始化去容器中找A对象的时候,此时A对象还没有创建完成,所以又会去重复地创建A对象,从而形成了一个闭环。所以要解决循环依赖问题首先要考虑的就是如何去打破这个闭环,考虑一下是否可以再引入一个缓存来存放已经完成实例化的对象?如下图:
在这里插入图片描述
新加了个缓存之后,对象的创建流程就发生了一定的改变:当A对象初始化去缓存map中找B对象的时候,B对象是不存在的;所以就会接下来创建B对象,当给B对象进行初始化的时候,此时A对象已经在缓存当中,尽管此时的A还没有完成属性填充,但是B对象还是可以完成初始化操作的,从而打破了闭环~~
到了这里可能就会有个疑问,为什么要额外引入一个缓存,我们就用一级缓存不是也可以完成这种需求吗??我们来看一下spring获取bean的源码:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// 先从一级缓存中获取
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			//如果一级缓存中没就从二级缓存中拿
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				//二级缓存没有就从三级缓存中拿
				//这里加了同步锁是为了防止多线程情况下对象重复创建的问题
				synchronized (this.singletonObjects) {
					singletonObject = this.singletonObjects.get(beanName);
					if (singletonObject == null) {
						singletonObject = this.earlySingletonObjects.get(beanName);
						if (singletonObject == null) {
							ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
							if (singletonFactory != null) {
								singletonObject = singletonFactory.getObject();
								this.earlySingletonObjects.put(beanName, singletonObject);
								this.singletonFactories.remove(beanName);
							}
						}
					}
				}
			}
		}
		return singletonObject;
	}

getSingleton方法是spring中获取单例对象的关键逻辑,前面有提到过,一级缓存存放的是已经完成创建的单例Bean,而且当spring去获取bean的时候一定是先去访问一级缓存,如果把仅仅实例化好的对象放进去,万一被其他对象拿起来就用就可能造成空指针异常。
同时,还有个最大的疑问,当我们引入一个缓存之后,循环依赖的问题貌似已经解决了呀!!!那spring为什么还要引入一个三级缓存呢??这就需要我们深入源码才能搞清楚这个问题。

源码

我是将spring源码下载到本地编译完导入idea,然后直接在源码上进行分析调试,感兴趣的话可以阅读一下我之前的博客grade+idea编译spring源码。相关的测试代码如下:
MustService源码

package com.spring.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MustService {
	@Autowired
	private TestService testService;
	public void invokeA() {
		testService.printA();
	}
	public void printB() {
		System.out.println("B");
	}
}

TestService源码

package com.spring.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TestService {
	@Autowired
	MustService mustService;
	public void invokeB() {
		mustService.printB();
	}
	public void printA() {
		System.out.println("A");
	}
}

AppConfig源码

package com.spring.demo;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@ComponentScan("com.spring.demo")
@Configuration
public class AppConfig {
}

main方法

package com.spring.demo;

import com.spring.demo.service.TestService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
	}
}

测试代码中一共定义了两个Servie和一个Config,MustServiceTestService互相依赖形成循环依赖。在说循环依赖之前,我这里先简单给出个spring创建bean的时序图,大家可以跟着这个时序图先去感受一下:
在这里插入图片描述
doGetBean之前着实没什么好说的,IOC容器在初始化是一定会调用refresh方法,紧接着经过一系列步骤会走到getBean,从这开始spring就会开始真正的创建bean的逻辑。getBean方法没有其他逻辑,直接调用了doGetBean方法,在spring源码中,一旦看到以do开头的方法一定要引起重视,因为它是真正干事的方法!!doGetBean方法会两次调用getSingleton方法,第一次调用仅仅是从缓存中获取对象(源码在前面的部分已经讲过),如果对象为null,就会接着第二次调用getSingleton方法(本文说的全都是单例对象),与第一次不同的是,第二次getSingleton方法传入的参数是beanName和函数式接口ObjectFactory<?>,我们再回顾一下之前说过的三级缓存,第三级缓存的value就是这个函数式接口!我们可以看一下这个类的源码:

@FunctionalInterface
public interface ObjectFactory<T> {
	T getObject() throws BeansException;
}

ObjectFactory这个类中只有一个方法,而且当调用这个getObject方法时,就会回调我们传入的lambda表达式!那我们再来看一下第二次getSingleton传入的是什么:

sharedInstance = getSingleton(beanName, () -> {
	try {
		//创建一个指定Bean实例对象
		return createBean(beanName, mbd, args);
	}
	catch (BeansException ex) {
		destroySingleton(beanName);
		throw ex;
	}
});

虽然我现在不知道这个getSingleton方法的具体逻辑,但我知道,如时序图所示,一旦第一次没有获取到对象,那么这一次一定会走到lambda表达式中的createBean方法开始走创建流程。在doCreateBean创建对象的过程中,主要有三个关键步骤:

  • 第一步:createBeanInstance
    在这个方法中利用反射获取到bean到无参构造,然后newInstance实例化对象
  • 第二步:addSingletonFactory提前暴露对象
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
	Assert.notNull(singletonFactory, "Singleton factory must not be null");
	synchronized (this.singletonObjects) {
		if (!this.singletonObjects.containsKey(beanName)) {
			this.singletonFactories.put(beanName, singletonFactory);
			this.earlySingletonObjects.remove(beanName);
			this.registeredSingletons.add(beanName);
		}
	}
}

这个addSingletonFactory方法传入的又是一个函数式接口,并存到第三级缓存,这一步spring已经开始为解决循环依赖做准备了!

  • 第三步:populateBean填充属性
    填充完属性之后,如果不考虑后续的awareprocessor一个完成中bean对象已经被创建出来了。

我们正在研究的是基于属性赋值的循环依赖,所以我们基于populateBean讲解循环依赖的处理逻辑,附上一张方法的时序图:
在这里插入图片描述
由时序图发现:populateBean方法中,辗转反侧了一系列方法最后又是回到了我们常说的getBean方法。以我们的测试代码为例,当为MustService填充属性时,会去容器中寻找TestService,此时一定是不存在的,所以会接着创建TestService,而且不可避免的接下来一定会为TestService的属性mustService赋值,重点来了,我们一定会通过getSingleton方法从缓存中获取MustService对象,此时这个对象已经在三级缓存中,再来重温一下这个方法的源码,发现有一段代码:

ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
	singletonObject = singletonFactory.getObject();
	this.earlySingletonObjects.put(beanName, singletonObject);
	this.singletonFactories.remove(beanName);
}

从三级缓存中取出ObjectFactory,然后调用它的getObject方法,我们之前就说过这个方法的调用一定会去回调另外一个我们传入的方法。根据addSingletonFactory方法,spring前期往三级缓存放的lambda表达式是() -> getEarlyBeanReference(beanName, mbd, bean),所以代码一定会走到getEarlyBeanReference方法,这个方法的细节这里先不说,只需要知道反正这个方法是返回了一个对象,然后将对象存到二级缓存(第二次调用的getSingleton方法在createBean完成之后会将对象存入一级缓存),最后从三级缓存中删除!这样就可以通过缓存来解决循环依赖了~~但是还记得我们之前的疑问吗?为什么要三级缓存,二级缓存为什么不行?
我们来做个试验,在createBeanInstance完成之后直接把对象存入二级缓存会是什么结果,修改源码如下:
在这里插入图片描述
改完运行结果如下:
在这里插入图片描述
竟然是运行成功的!!我们去查阅资料会发现三级缓存是跟aop有关的,那我们加上aop的逻辑,代码如下:

@ComponentScan("com.spring.demo")
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

@Aspect
@Component
public class AopService {
	@Pointcut("execution(* com.spring.demo.service.TestService.invokeB())")
	public void pointCut() {

	}
	@Pointcut("execution(* com.spring.demo.service.MustService.*())")
	public void must() {
	}
	@Before("pointCut()")
	public void before() {
		System.out.println("======== aop active ========");
	}
	@Before("must()")
	public void after() {
		System.out.println("============================");
	}
}

分别对两个类加上aop代理,再来运行看看结果:
在这里插入图片描述
报错来了,重点看报错内容中的这一段内容:

Bean with name 'mustService' has been injected into other beans [testService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. 

大体意思是注入的属性值不是最终版本!!这时候我们就需要从bean的生命周期入手,aop发生的阶段是BeanPostProcessor,而在这之前bean已经完成属性注入了,所以注入的一定是原始对象而非代理对象,而代理对象创建出来会将原始对象替换掉,所以问题就来了。
这时候我们再回头看一下getEarlyBeanReference这个方法到底实现了什么,按照顺序一步步往下深入getEarlyBeanReference->getEarlyBeanReference->wrapIfNecessary->createProxy->getProxy,原来这个方法是用来创建代理对象的!!!再来看一下getProxy的实现类:
在这里插入图片描述
创建代理的两种方式:cglib和jdk,熟悉吧~~
所以三级缓存的目的是当我们第一次需要去get这个bean的时候,就能够确保获取到的就是最终形态的对象!!好像又有问题了,从三级缓存生成的对象为什么不直接放到一级缓存,干嘛非要放到二级缓存呢?还是看spring bean的生命周期,这个时候bean的生命还远没有走完呢,所以bean还是不完整的,就可以用二级缓存做过渡~~

结语

spring源码是非常庞大的,类非常多,再加上父子容器,所以看源码的时候很容易晕,大家可以跟着文章和时序图多去debug走几遍,看的多了自然感觉就来了,毕竟spring源码的阅读不是朝夕之事~~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值