那些年背过的题:Spring Bean实例化-循环依赖问题

在Spring框架中,循环依赖问题是指两个或多个bean相互依赖,形成一个循环引用。这个问题在使用依赖注入时比较常见。理解如何识别和解决这个问题对于开发者来说很重要。

什么是循环依赖?

循环依赖简单来说就是A依赖B,而B又依赖A。例如:

java 复制代码
@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

在上面的例子中,A依赖于B,同时B也依赖于A,这就形成了一个循环依赖。

解决循环依赖的方法

  1. Setter注入(Setter Injection) : 使用setter方法进行依赖注入,可以有效地解决大部分循环依赖问题。Spring先实例化bean,然后再进行属性填充,这样可以打破循环依赖

    java 复制代码
    @Component
    public class A {
        private B b;
    
        @Autowired
        public void setB(B b) {
            this.b = b;
        }
    }
    
    @Component
    public class B {
        private A a;
    
        @Autowired
        public void setA(A a) {
            this.a = a;
        }
    }
  2. @Lazy注解 : 使用@Lazy注解延迟加载bean,使得Spring在实际需要bean的时候才进行初始化,从而避免循环依赖。

    java 复制代码
    @Component
    public class A {
        private final B b;
    
        @Autowired
        public A(@Lazy B b) {
            this.b = b;
        }
    }
    
    @Component
    public class B {
        private final A a;
    
        @Autowired
        public B(@Lazy A a) {
            this.a = a;
        }
    }
  3. 构造器注入(Constructor Injection)如果使用构造器注入,则无法直接解决循环依赖问题,因为Spring在创建bean时需要先解析构造函数的参数,存在循环依赖的话,无法完成实例化。这种情况下,需要重新设计类之间的关系,或者使用其他方式,如工厂模式来间接解决。

  4. ApplicationContextAware接口 : 通过实现ApplicationContextAware接口,可以手动从Spring上下文中获取依赖bean。这种做法虽然能解决问题,但会增加代码复杂度,不推荐广泛使用。

    java 复制代码
    @Component
    public class A implements ApplicationContextAware {
        private B b;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.b = applicationContext.getBean(B.class);
        }
    }
    
    @Component
    public class B {
        private final A a;
    
        @Autowired
        public B(A a) {
            this.a = a;
        }
    }
  5. 重新设计: 如果代码中出现循环依赖问题,说明代码结构设计不合理,没有做好分层工作;架构设计应该遵循:上层依赖下层,下层不应该反向依赖上层;如果存在共用部分,应该放到抽象层,上下层共同依赖抽象层。

三级缓存机制

Spring主要通过三级缓存机制来解决循环依赖问题。在Spring的DefaultSingletonBeanRegistry类中,维护了三个缓存:

  1. singletonObjects:一级缓存,用于存放完全初始化好的单例bean。
  2. earlySingletonObjects:二级缓存,用于存放那些实例化完成但未初始化完成的早期单例bean。
  3. singletonFactories:三级缓存,用于存放能够创建早期单例bean的工厂。

三级缓存singletonFactories的作用

三级缓存的核心思想是,尽早暴露对象的引用,以便其他bean可以依赖这些尚未完全初始化的bean,从而解决循环依赖的问题。

源码分析

以下是Spring解决循环依赖的关键步骤:

  1. 实例化Bean: 当Spring创建一个bean时,会先检查这个bean是否已经被创建。如果没有,则实例化该bean(调用构造函数)。

    java 复制代码
    protected Object createBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) {
        //...省略部分代码
        try {
            // 第一步:实例化Bean
            instanceWrapper = createBeanInstance(beanName, mbd, args);
            bean = instanceWrapper.getWrappedInstance();
            beanType = instanceWrapper.getWrappedClass();
            
            // 判断是否允许提前暴露
            boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                    isSingletonCurrentlyInCreation(beanName));
            if (earlySingletonExposure) {
                addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
            }
        } 
        //...省略部分代码
    }
  2. 提前暴露Bean引用 : Spring会将实例化后的bean包装成一个ObjectFactory放入三级缓存singletonFactories中。这样,如果另一个bean在依赖注入过程中需要这个bean,可以通过ObjectFactory获取到早期引用。

    java 复制代码
    protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
        synchronized (this.singletonObjects) {
            if (!this.singletonObjects.containsKey(beanName)) {
                this.singletonFactories.put(beanName, singletonFactory);
                this.earlySingletonObjects.remove(beanName);
                this.registeredSingletons.add(beanName);
            }
        }
    }
  3. 属性填充和初始化: 接下来,Spring会进行属性填充和初始化。这时候,如果有其他bean依赖当前bean,就会通过三级缓存获取早期引用。

    java 复制代码
    protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
            throws BeanCreationException {
        //...省略部分代码
        populateBean(beanName, mbd, instanceWrapper);
        exposedObject = initializeBean(beanName, exposedObject, mbd);
        //...省略部分代码
    }
  4. 从缓存中获取Bean : 如果在依赖注入过程中发现需要一个尚未完全初始化的bean,Spring会首先从一级缓存singletonObjects中查找,如果找不到,再从二级缓存earlySingletonObjects中查找,最后从三级缓存singletonFactories中查找并通过工厂方法创建早期引用。

    java 复制代码
    @Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
                singletonObject = this.earlySingletonObjects.get(beanName);
                if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return singletonObject;
    }

总结

通过三级缓存机制,Spring能够缓解因为循环依赖导致的bean创建失败问题。在bean实例化后,Spring会将其放入三级缓存中的工厂里,从而使得其他依赖它的bean能够获取到其早期引用。随后在属性填充和初始化阶段,Spring会逐步将bean移动到二级缓存和一级缓存中,从而最终解决循环依赖问题。

示例说明

java 复制代码
@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

当Spring IOC容器初始化上述两个bean时,三级缓存机制会按如下步骤工作:

  1. 创建A

    • A 正在创建中,将其标记并放入三级缓存。
  2. 注入B到A

    • Spring发现需要注入B,于是开始创建B
  3. 创建B

    • B 正在创建中,将其标记并放入三级缓存。
  4. 注入A到B

    • 由于A正在创建中,Spring会从三级缓存中获取A的ObjectFactory,通过该工厂返回一个早期暴露的A(代理对象或部分初始化的对象),并将其放入二级缓存。
  5. 完成B的创建并将其放入一级缓存

    • 注入完成后,B的创建过程结束,将其放入一级缓存。
  6. 完成A的创建并将其放入一级缓存

    • 最终,A的创建过程结束,将其从三级缓存移出并放入一级缓存。

通过这种机制,Spring可以在处理构造器循环依赖时成功初始化所有bean而不引发无限递归。

相关推荐
刘大辉在路上1 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
测试老哥2 小时前
外包干了两年,技术退步明显。。。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
追逐时光者3 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~3 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581364 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳4 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾4 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
ThisIsClark4 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
星就前端叭5 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc