第12节 Spring源码之 Bean 的循环依赖

循环依赖是 Spring 中经典问题之一,那么到底什么是循环依赖?简单说就是对象之间相互引用, 如下图所示:

代码层面上很好理解,在 bean 创建过程中 class Aclass B 又经历了怎样的过程呢?

可以看出形成了一个闭环,如果想解决这个问题,那么在属性填充时要保证不二次创建 A对象 的步骤,也就是必须保证从容器中能够直接获取到 B。

一、复现循环依赖问题

Spring 中默认允许循环依赖的存在,但在 Spring Boot 2.6.x 版本开始默认禁用了循环依赖

1. 基于xml复现循环依赖

  • 定义实体 Bean
java 复制代码
public class A {

   private B b;

   public B getB() {
      return b;
   }

   public void setB(B b) {
      this.b = b;
   }
}
java 复制代码
public class B {

   private A a;

   public A getA() {
      return a;
   }

   public void setA(A a) {
      this.a = a;
   }
}
  • 重写 customizeBeanFactory 方法,禁止循环依赖
java 复制代码
public class NotAllowCircularXmlApplicationContext extends ClassPathXmlApplicationContext {

   public NotAllowCircularXmlApplicationContext(String... configLocations) throws BeansException {
      super(configLocations);
   }

   @Override
   protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
      // 禁止循环依赖
      super.setAllowCircularReferences(false);
      super.customizeBeanFactory(beanFactory);
   }
}
  • 配置 xml 文件
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:context="http://www.springframework.org/schema/context"
      xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
            http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd">

   <context:component-scan base-package="com.sff.demo.cycle"/>

   <bean id="a" class="com.sff.demo.cycle.A">
      <property name="b" ref="b"/>
   </bean>
    
    <bean id="b" class="com.sff.demo.cycle.B">
      <property name="a" ref="a"/>
   </bean>
</beans>
java 复制代码
public class TestAnnoMain {

   public static void main(String[] args) {
      testCycleRef();
   }

   public static void testXmlCycleRef() {
      NotAllowCircularXmlApplicationContext ac = new NotAllowCircularXmlApplicationContext("application-cycle.xml");
      A bean = ac.getBean(A.class);
      System.out.println(bean);
   }
}   

运行结果:

2. 基于注解 @Autowired 复现循环依赖

  • 实体定义
java 复制代码
@Component
public class C {

   @Autowired
   private D d;
}

@Component
public class D {

   @Autowired
   private C c;
}
  • 重写 AnnotationConfigApplicationContext,禁止循环依赖
java 复制代码
public class NotAllowCircularAnnotationConfigApplicationContext extends AnnotationConfigApplicationContext {

   public NotAllowCircularAnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
      super();
      register(annotatedClasses);

      // 禁止循环依赖
      super.setAllowCircularReferences(false);

      refresh();
   }
}
  • 运行结果
java 复制代码
public class TestAnnoMain {

   public static void main(String[] args) {
      testCycleRef();
   }

   public static void testCycleRef() {
      NotAllowCircularAnnotationConfigApplicationContext ac = new NotAllowCircularAnnotationConfigApplicationContext(BeanConfig3.class);
      C bean = ac.getBean(C.class);
      System.out.println(bean);
   }
}   

二、在 Spring 中是如何解决该问题的呢?

Spring 中利用三级缓存解决循环依赖问题,缓存定义在 DefaultSingletonBeanRegistry

java 复制代码
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

    // 一级缓存,保存 beanName 和 实例化、初始化好的完整 bean 对象
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    // 二级缓存,保存 beanName 和 未初始化的 bean 对象
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);  

    // 三级缓存,保存 beanName 和 lambda 表达式 () -> getEarlyBeanReference(beanName, mbd, bean)
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

}

Spring 中利用三级缓存解决循环依赖的根本就是为了避免引用对象的循环创建,那么它是如何实现这一目的呢?

在本文案例中 class A 中 引用 class B ,同时在 class B 中引用 class A,在创建 A 对象时通过 addSingletonFactory方法,

java 复制代码
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

在三级缓 singletonFactories.put(beanName,() -> getEarlyBeanReference(beanName, mbd, bean)) 中存放了 A 对象的这样一个 lambda 表达式,可以理解成回调函数。

当在创建 B 对象时,会调用通过 getSingleton 方法调用A对象三级缓存中的 lambda 表达式,也就是getEarlyBeanReference(beanName, mbd, bean) ,在没有AOP代理的情况下,可以直接返回 A 的对象信息,也就 getEarlyBeanReference(beanName, mbd, bean) 的参数 bean 就是提前缓存的 A 对象。

总结: 在 Spring 中 ,对象的创建分为实例化和初始化,实例化好但是未完成初始化的对象可以直接给其他对象引用,所以 Spring 中就把实例化好为初始化好的对象提前暴露出去,让其他对象能够进行引用,就完成了循环依赖的解耦!

相关推荐
uhakadotcom38 分钟前
AI搜索引擎的尽头是电商?从perplexity开始卖货说起...
前端·人工智能·后端
uhakadotcom41 分钟前
Java中的代码简化技巧:让开发更轻松
后端
张声录11 小时前
使用client-go在命令空间test里面对pod进行操作
开发语言·后端·golang
新智元2 小时前
AI卷翻科研!DeepMind 36页报告:全球实验室被「AI科学家」指数级接管
人工智能·后端
跳跳的向阳花2 小时前
03-06、SpringCloud第六章,升级篇,升级概述与Rest微服务案例构建
spring·spring cloud·微服务
Adolf_19932 小时前
Django 自定义路由转换器
后端·python·django
奔跑吧邓邓子2 小时前
SpringCloud之Eureka:服务注册与发现全面教程!
spring·spring cloud·eureka
王·小白攻城狮·不是那么帅的哥·天文3 小时前
Spring框架使用xml方式配置ThreadPoolTaskExecutor线程池,并且自定义线程工厂
xml·java·spring·线程池
ᝰꫝꪉꪯꫀ3613 小时前
JavaWeb——Mybatis
java·开发语言·后端·mybatis
机器之心3 小时前
跨模态大升级!少量数据高效微调,LLM教会CLIP玩转复杂文本
人工智能·后端