那些年背过的题: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而不引发无限递归。

相关推荐
monkey_meng20 分钟前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
草莓base33 分钟前
【手写一个spring】spring源码的简单实现--bean对象的创建
java·spring·rpc
Estar.Lee35 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
乌啼霜满天2491 小时前
Spring 与 Spring MVC 与 Spring Boot三者之间的区别与联系
java·spring boot·spring·mvc
新知图书1 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
Elaine2023911 小时前
零碎04 MybatisPlus自定义模版生成代码
java·spring·mybatis
盛夏绽放2 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang2 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net
Rverdoser3 小时前
RabbitMQ的基本概念和入门
开发语言·后端·ruby
Tech Synapse4 小时前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端