什么是循环依赖
A 类中有一个属性 B ,也就是说 A 依赖 B,同时 B 类中有一个属性 A, 也就是说 B 依赖 A. 他们之间的依赖关系形成了环。就是我们说的循环依赖,如下图:
循环依赖示例
public class CircularDependenciesDemo {
public static void main(String[] args) {
new A1();
}
}
class A1 {
private B1 b1;
public A1() {
this.b1 = new B1();
}
}
class B1 {
private A1 a1;
public B1() {
this.a1 = new A1();
}
}
结果:
Exception in thread "main" java.lang.StackOverflowError
at io.dc.B1.<init>(CircularDependenciesDemo.java:23)
at io.dc.A1.<init>(CircularDependenciesDemo.java:16)
at io.dc.B1.<init>(CircularDependenciesDemo.java:24)
如上所示,发生 栈溢出错误。下面我们试着解决这种循环依赖
解决循环依赖
public class CircularDependenciesDemo {
// 实例缓存池
public static Map<String, Object> existsObject = new HashMap<>();
public static Object loadObject (String objectName) {
if(existsObject.containsKey(objectName)) {
return existsObject.get(objectName);
}
if("A1".equals(objectName)) {
A1 a1 = new A1();
// 先将空白的实例放入缓存
existsObject.put("A1",a1);
// 然后将对该空白实例进行属性赋值
a1.setB1((B1)loadObject("B1"));
return a1;
}
if ("B1".equals(objectName)) {
B1 b1 = new B1();
existsObject.put("B1",b1);
b1.setA1((A1)loadObject("A1"));
return b1;
}
return null;
}
public static void main(String[] args) {
A1 a1 = (A1)loadObject("A1");
a1.getB1().sayHello();
}
}
class A1 {
private B1 b1;
public B1 getB1() {
return b1;
}
public void setB1(B1 b1) {
this.b1 = b1;
}
}
class B1 {
private A1 a1;
public A1 getA1() {
return a1;
}
public void setA1(A1 a1) {
this.a1 = a1;
}
public void sayHello(){
System.out.println("我是 B1 , 你好");
}
}
结果:
我是 B1 , 你好
本来由构造方法来构造 A1 中的 B1. 现在分成两步:
- 先调用无参构造器生成一个空白实例
- 再调用 set 方法,为空白实例赋值
为了解决循环依赖,设置了一个实例缓存池,existsObject. 用来存放已经生成的实例。但是这个实例池会存放空白对象的状态。在多线程的情况下,会取到一个空白实例。也就是对象中的字段都是 null, 引发程序错误。我们可以再添加一层二级缓存,二级缓存中存放空白实例。一级缓存中只放完整实例。
纯净的缓存
public class CircularDependenciesDemo {
// 一级缓存,只存放完整的对象
public static Map<String, Object> existsObject = new HashMap<>();
// 二级缓存,包含空白的对象
public static Map<String, Object> blankObject = new HashMap<>();
// 加载对象
public static Object loadObject (String objectName) {
// 先从一级缓存取
if(existsObject.containsKey(objectName)) {
return existsObject.get(objectName);
}
// 再从二级缓存取
if(blankObject.containsKey(objectName)) {
return blankObject.get(objectName);
}
if("A1".equals(objectName)) {
A1 a1 = new A1();
// 先将空白的实例放入二级缓存
blankObject.put("A1",a1);
// 然后将对该空白实例进行属性赋值
a1.setB1((B1)loadObject("B1"));
// 再放入一级缓存
existsObject.put("A1",a1);
return a1;
}
if ("B1".equals(objectName)) {
B1 b1 = new B1();
blankObject.put("B1",b1);
b1.setA1((A1)loadObject("A1"));
existsObject.put("B1",b1);
return b1;
}
return null;
}
public static void main(String[] args) {
A1 a1 = (A1)loadObject("A1");
a1.getB1().sayHello();
B1 b1 = (B1) loadObject("B1");
b1.getA1().sayHello();
}
}
class A1 {
private B1 b1;
public B1 getB1() {
return b1;
}
public void setB1(B1 b1) {
this.b1 = b1;
}
public void sayHello(){
System.out.println("我是 A1 , 你好");
}
}
class B1 {
private A1 a1;
public A1 getA1() {
return a1;
}
public void setA1(A1 a1) {
this.a1 = a1;
}
public void sayHello(){
System.out.println("我是 B1 , 你好");
}
}
通过添加二级缓存,把空白对象和完整对象剥离了。当从一级缓存中取实例时,要么拿到的是完整对象,要么拿到的是 null, 而不会获取到空白的对象,引发错误。
但是上面的代码是有问题的,当实例未初始化完,并且在在多线程的情况下,仍然会取到不完整对象,如下:
那么单单只加二级缓存并不能解决这个并发问题,同时还要加锁:
public static Object loadObject (String objectName) {
// 先从一级缓存取,因为可以确保一级缓存中只有完整的对象
if(existsObject.containsKey(objectName)) {
return existsObject.get(objectName);
}
synchronized (existsObject) {
// 第二次判断一级缓存是否有,因为有可能在等待锁的时候,已经有其他线程把实例放入一级缓存中
if(existsObject.containsKey(objectName)) {
return existsObject.get(objectName);
}
// 再从二级缓存取
if(blankObject.containsKey(objectName)) {
return blankObject.get(objectName);
}
if("A1".equals(objectName)) {
A1 a1 = new A1();
// 先将空白的实例放入二级缓存
blankObject.put("A1",a1);
// 然后将对该空白实例进行属性赋值
a1.setB1((B1)loadObject("B1"));
// 再放入一级缓存
existsObject.put("A1",a1);
// 然后移除二级缓存中的 A1
blankObject.remove("A1");
return a1;
}
if ("B1".equals(objectName)) {
B1 b1 = new B1();
blankObject.put("B1",b1);
b1.setA1((A1)loadObject("A1"));
existsObject.put("B1",b1);
blankObject.remove("B1");
return b1;
}
}
return null;
}
到此我们才完整的解决了循环依赖,并且保证不会取到不完整的实例.
其实 spring 也是这样来解决bean循环依赖的,不过 spring 还添加动态代理bean的功能,为了解耦,还添加了三级缓存,保证代码整洁,优雅。
spring 是如何解决循环依赖的
由于 spring 在处理循环依赖时考虑很多其他功能,代码非常复杂,为了便于展示,这里只模仿核心功能:
详细说明已在注释中
// 主类
public class MainStart {
// 本应该使用 ConcurrentHashMap 类型,但是为了演示效果,确保先实例化A,使用了有序HashMap
private static Map<String, BeanDefinition> beanDefinitionMap = new LinkedHashMap<>(256);
/**
* 读取bean定义,当然在spring中肯定是根据配置 动态扫描注册
*/
public static void loadBeanDefinitions() {
RootBeanDefinition aBeanDefinition=new RootBeanDefinition(InstanceA.class);
RootBeanDefinition bBeanDefinition=new RootBeanDefinition(InstanceB.class);
beanDefinitionMap.put("instanceA",aBeanDefinition);
beanDefinitionMap.put("instanceB",bBeanDefinition);
}
public static void main(String[] args) throws Exception {
// 加载了BeanDefinition
loadBeanDefinitions();
// 循环创建Bean
for (String key : beanDefinitionMap.keySet()){
// 先创建A
getBean(key);
}
IApi instanceA = (IApi) getBean("instanceA");
instanceA.say();
}
// 一级缓存
public static Map<String,Object> singletonObjects=new ConcurrentHashMap<>();
// 二级缓存: 为了将 成熟Bean和纯净Bean分离,避免读取到不完整得Bean
public static Map<String,Object> earlySingletonObjects=new ConcurrentHashMap<>();
// 三级缓存
public static Map<String,ObjectFactory> singletonFactories=new ConcurrentHashMap<>();
// 循环依赖标识
public static Set<String> singletonsCurrennlyInCreation=new HashSet<>();
// 假设A 使用了Aop @PointCut("execution(* *..InstanceA.*(..))") 要给A创建动态代理
// 获取Bean
public static Object getBean(String beanName) throws Exception {
Object singleton = getSingleton(beanName);
if(singleton!=null){
return singleton;
}
Object instanceBean = null;
synchronized (singletonObjects) {
// 第二次判断
if(singletonObjects.containsKey(beanName)) {
return singletonObjects.get(beanName);
}
// 标记正在创建,而不是用二级缓存是否包含 beanName 来判断
if(!singletonsCurrennlyInCreation.contains(beanName)){
singletonsCurrennlyInCreation.add(beanName);
}
// 实例化
RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionMap.get(beanName);
// 获取 class 对象
Class<?> beanClass = beanDefinition.getBeanClass();
// 通过无参构造函数,构造一个空白对象
instanceBean = beanClass.newInstance();
// 创建动态代理
// 只在循环依赖的情况下在实例化后创建proxy 判断当前是不是循环依赖
Object finalInstanceBean = instanceBean;
// 这是一个三级缓存,三级缓存放的不是 bean, 而是一个可以创建动态代理bean的函数。(lambda 表达式。关键词: 函数接口)
// 为什么不直接创建一个代理对象放入二级缓存中,而是先把 lambda 表达式放入三级缓存,
// 而后面再调用这个表达式去生成代理对象,然后放入二级缓存(getSingleton 方法)
// 我认为是是为了逻辑上的统一,getSingleton方法负责创建或返回对象,而不是在这里。
singletonFactories.put(beanName, () -> new JdkProxyBeanPostProcessor().getEarlyBeanReference(finalInstanceBean,beanName));
// 属性赋值
Field[] declaredFields = beanClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
Autowired annotation = declaredField.getAnnotation(Autowired.class);
// 说明属性上面有Autowired
if(annotation!=null){
declaredField.setAccessible(true);
String name = declaredField.getName();
Object fileObject= getBean(name);
declaredField.set(instanceBean,fileObject);
}
}
// 由于递归完后A 还是原实例,, 所以要从二级缓存中拿到proxy 。
if(earlySingletonObjects.containsKey(beanName)){
instanceBean=earlySingletonObjects.get(beanName);
}
// 添加到一级缓存 A
singletonObjects.put(beanName,instanceBean);
// remove 二级缓存和三级缓存
earlySingletonObjects.remove(beanName);
singletonFactories.remove(beanName);
}
return instanceBean;
}
public static Object getSingleton(String beanName){
// 先从一级缓存中拿
Object bean = singletonObjects.get(beanName);
synchronized (singletonObjects) {
// 说明是循环依赖
if(bean==null && singletonsCurrennlyInCreation.contains(beanName)){
bean=earlySingletonObjects.get(beanName);
// 如果二级缓存没有就从三级缓存中拿
if(bean==null) {
// 从三级缓存中拿
ObjectFactory factory = singletonFactories.get(beanName);
if (factory != null) {
// 拿到动态代理
// 为什么需要动态代理对象,而不是我们自己原来的bean.
// 因为原始bean的代理对象扩展了功能,同时还和原始 bean 有相同的类型。因为它们继承了同一个接口
// 或者 代理对象继承了原始bean. (详情可以搜索 JDK动态代理,cglib 代理)
bean=factory.getObject();
// 将代理对象放入二级缓存,这个代理对象仍然是空白对象
// 所以这个方法如果不加锁仍然可能会返回一个空白对象
earlySingletonObjects.put(beanName, bean);
}
}
}
}
return bean;
}
}
// InstanceA 接口, 为了动态代理使用
public interface IApi {
void say();
}
// InstanceA
@Component
public class InstanceA implements IApi {
@Autowired
private InstanceB instanceB;
public InstanceB getInstanceB() {
return instanceB;
}
public void setInstanceB(InstanceB instanceB) {
this.instanceB = instanceB;
}
public InstanceA(InstanceB instanceB) {
this.instanceB = instanceB;
}
public InstanceA() {
System.out.println("InstanceA实例化");
}
@Override
public void say() {
System.out.println("I'm A");
}
}
// InstanceB
@Component
public class InstanceB {
@Autowired
private InstanceA instanceA;
public InstanceA getInstanceA() {
return instanceA;
}
public void setInstanceA(InstanceA instanceA) {
this.instanceA = instanceA;
}
public InstanceB(InstanceA instanceA) {
this.instanceA = instanceA;
}
public InstanceB() {
System.out.println("InstanceB实例化");
}
}
// JDK 动态代理使用
public class JdkDynimcProxy implements InvocationHandler {
private Object target;
public JdkDynimcProxy(Object target) {
this.target = target;
}
public <T> T getProxy() {
return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("测试");
return method.invoke(target,args);
}
}
//
@Component
public class JdkProxyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor {
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
// 假设:A 被切点命中 需要创建代理 @PointCut("execution(* *..InstanceA.*(..))")
if(bean instanceof InstanceA) {
JdkDynimcProxy jdkDynimcProxy = new JdkDynimcProxy(bean);
return jdkDynimcProxy.getProxy();
}
return bean;
}
}
为什么需要三级缓存,而不是两级缓存
我认为两级缓存完全可以解决循环依赖,完全可以先创建 bean的动态代理放入二级缓存中,而不是在 getSingleton 方法中延迟调用三级缓存中的 lambda 表达式再去生成动态代理。
我认为三级缓存作用之一是为了代码解耦,逻辑统一。
spring 如何避免拿到不完整的bean
- spring 容器加载完成后,因为解决了循环依赖不会存在不完整的bean,
- 在 spring 容器加载过程中,通过加锁的方式可以避免取到不完整的bean
spring 没有解决的循环依赖
- 没有解决构造函数的循环依赖
所以不建议构造函数注入方式 - 没有解决多例下的循环依赖
好像也无法解决,没有必要