欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > 【2025最近Java面试八股】Spring中循环依赖的问题?怎么解决的?

【2025最近Java面试八股】Spring中循环依赖的问题?怎么解决的?

2025/4/28 10:18:41 来源:https://blog.csdn.net/gwndjsh/article/details/147537595  浏览:    关键词:【2025最近Java面试八股】Spring中循环依赖的问题?怎么解决的?
1. 什么是循环依赖?

在Spring框架中,循环依赖是指两个或多个bean之间相互依赖,形成了一个循环引用的情况。如果不加以处理,这种情况会导致应用程序启动失败。导致 Spring 容器无法完成依赖注入。
例如:

@Service
public class A {@Autowiredprivate B b;
}@Service
public class B {@Autowiredprivate A a;
}

此时,A 依赖 BB 又依赖 A,Spring 无法确定先初始化哪个 Bean。

要解决循环依赖问题的限制

Spring解决循环依赖是有一定限制的:
首先就是要求互相依赖的Bean必须要是单例的Bean;

        为什么呢?Spring循环依赖的解决方案主要是通过对象的提前暴露来实现的。当一个对象在创建过程中需要引用到另一个正在创建的对象时,Spring会先提前暴露一个尚未完全初始化的对象实例,以解决循环依赖的问题。这个尚未完全初始化的对象实例就是半成品对象。

在 Spring 容器中,单例对象的创建和初始化只会发生一次,并且在容器启动时就完成了。这意味着,在容器运行期间,单例对象的依赖关系不会发生变化。因此,可以通过提前暴露半成品对象的方式来解决循环依赖的问题。

相比之下,原型对象的创建和初始化可以发生多次,并且可能在容器运行期间动态地发生变化。因此,对于原型对象,提前暴露半成品对象并不能解决循环依赖的问题,因为在后续的创建过程中,可能会涉及到不同的原型对象实例,无法像单例对象那样缓存并复用半成品对象。

另外就是依赖注入的方式不能都是构造函数注入的方式

为什么呢?Spring无法解决构造函数的循环依赖,是因为在对象实例化过程中,构造函数是最先被调用的而此时对象还未完成实例化,无法注入一个尚未完全创建的对象,因此Spring容器无法在构造函数注入中实现循环依赖的解决,像下面这样

@Component

public class ClassA {

        private final ClassB classB;

        @Autowired

        public ClassA(@Lazy ClassB classB) {

                this.classB = classB;

        }

        // ...

}

@Component

public class ClassB {

        private final ClassA classA;

        @Autowired

        public ClassB(ClassA classA) {

                this.classA = classA;

         }

        // ...

}

但是这样可以通过一些方法解决的

1、重新设计,彻底消除循环依赖

循环依赖,一般都是设计不合理导致的,可以从根本上做一些重构,来彻底解决,

2、改成非构造器注入

可以改成setter注入或者字段注入。

3、使用@Lazy解决(解决构造器循环依赖的)
首先要知道 Spring利用三级缓存是无法解决构造器注入这种循环依赖的。

@Lazy 是Spring框架中的一个注解,用于延迟一个bean的初始化,直到它第一次被使用。在默认情况下,Spring容器会在启动时创建并初始化所有的单例bean。这意味着,即使某个bean直到很晚才被使用,或者可能根本不被使用,它也会在应用启动时被创建。@Lazy 注解就是用来改变这种行为的。

也就是说,当我们使用 @Lazy 注解时,Spring容器会在需要该bean的时候才创建它,而不是在启动时。这意味着如果两个bean互相依赖,可以通过延迟其中一个bean的初始化来打破依赖循环。

缺点:过度使用 @Lazy 可能会导致应用程序的行为难以预测和跟踪,特别是在涉及多个依赖和复杂业务逻辑的情况下。

下面是一些例子

@Lazy 可以用在bean的定义上或者注入时。以下是一些使用示例:

@Component

@Lazy

public class LazyBean {

// ...

}

在这种情况下,LazyBean 只有在首次被使用时才会被创建和初始化。

@Component

public class SomeClass {

        private final LazyBean lazyBean;

        @Autowired

        public SomeClass(@Lazy LazyBean lazyBean) {

                this.lazyBean = lazyBean;

        }

}

在这里,即使SomeClass在容器启动时被创建,LazyBean也只会在SomeClass实际使用LazyBean时才被初始化。


2. Spring 如何检测循环依赖?依赖三级缓存机制

Spring 在 创建 Bean 的流程 中会检查循环依赖,主要通过 三级缓存(3-level cache) 机制:

  1. 一级缓存(Singleton Objects)
    存放 完全初始化好的 Bean(成品对象)。

  2. 二级缓存(Early Singleton Objects)
    存放 半成品 Bean(已实例化但未完成属性注入)。

    而当一个对象只进行了实例化,但是还没有进行初始化时,我们称之为半成品对象。所以,所谓半成品对象,其实只是 bean 对象的一个空壳子,还没有进行属性注入和初始化。
  3. 三级缓存(Singleton Factories)
    存放 Bean 的工厂对象(用于生成代理对象,如 AOP 场景)。

如果 Spring 发现某个 Bean 正在创建中(存在于二级缓存),但又再次被依赖,则判定为循环依赖。


3. Spring 如何解决循环依赖?spring提供了一种三级缓存的机制

前面说过Spring 的三级缓存仅能解决 单例(Singleton)作用域 且 通过属性注入(@Autowired) 的循环依赖,核心步骤如下:

首先,Spring中Bean的创建过程其实可以分成两步,第一步叫做实例化,第二步叫做初始化

具体流程如下:

(1) 创建 Bean A 的流程
  1. 实例化 A(调用构造函数,生成原始对象)。

  2. 将 A 的工厂对象放入三级缓存(用于后续可能的 AOP 代理)。

  3. 注入 A 的依赖(发现需要 B)。

  4. 去容器中获取 B(触发 B 的创建)。

(2) 创建 Bean B 的流程
  1. 实例化 B(生成原始对象)。

  2. 将 B 的工厂对象放入三级缓存

  3. 注入 B 的依赖(发现需要 A)。

  4. 从三级缓存获取 A 的工厂,生成 A 的早期引用(可能是代理对象,半成品)并放入二级缓存。

  5. B 完成属性注入,变成一个完整 Bean,放入一级缓存。

(3) 回到 A 的创建流程
  1. 从二级缓存拿到 B 的早期引用(半成品),注入到 A。

  2. A 完成初始化,从二级缓存移除,放入一级缓存。

最终,A 和 B 都成功创建,且互相持有对方的代理或真实对象。

以下是DefaultSingletonBeanRegistry#getSingleton方法,代码中,包括一级缓存、二级缓存、三级缓存的处理逻辑,该方法是获取bean的单例实例对象的核心方法:

@Nullable

protected Object getSingleton(String beanName, boolean allowEarlyReference) {

        // 首先从一级缓存中获取bean实例对象,如果已经存在,则直接返回

        Object singletonObject = this.singletonObjects.get(beanName);

                if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {

                // 如果一级缓存中不存在bean实例对象,而且当前bean正在创建中,则从二级缓                存中获取bean实例对象

                        singletonObject = this.earlySingletonObjects.get(beanName);

                        if (singletonObject == null && allowEarlyReference) {

                        // 如果二级缓存中也不存在bean实例对象,并且允许提前引用,则需要在锁定一级缓存之前,

                        // 先锁定二级缓存,然后再进行一系列处理

synchronized (this.singletonObjects) {

                        // 进行一系列安全检查后,再次从一级缓存和二级缓存中获取bean实例对象

singletonObject = this.singletonObjects.get(beanName);

                                if (singletonObject == null) {

                                        singletonObject = this.earlySingletonObjects.get(beanName);

                                                if (singletonObject == null) {

                                                        // 如果二级缓存中也不存在bean实例对象,则从三级缓存中获取bean的ObjectFactory,并创建bean实例对象

                                                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);

                                                if (singletonFactory != null) {

                                                        singletonObject = singletonFactory.getObject();

                                                        // 将创建好的bean实例对象存储到二级缓存中

                                                        this.earlySingletonObjects.put(beanName, singletonObject);

                                                        // 从三级缓存中移除bean的ObjectFactory

                                                        this.singletonFactories.remove(beanName);

                                                  }

                                          }

                                }

                        }

                }

        }

return singletonObject;

}

Spring解决循环依赖一定需要三级缓存吗?()

上面的流程可以看见只用到了Spring二级缓存就能解决依赖注入的问题。

其实,在大多数简单的循环依赖场景(没有AOP代理)中,二级缓存(Early Singleton Objects) 已经足够解决问题。但 Spring 仍然使用 三级缓存(Singleton Factories,存放 Bean 的工厂对象(用于生成代理对象,如 AOP 场景),主要是为了处理 AOP 代理 等特殊情况,确保返回的 Bean 是经过完整代理增强的对象。AOP又是Spring中很重要的一个特性,代理不能忽略。

所以三级缓存(Singleton Factories)的核心作用是 处理 AOP 代理,确保返回的 Bean 是代理对象而非原始对象。

(1)AOP 代理的问题
如果 Bean 需要被代理(如 @Transactional、@Async),Spring 不能直接返回原始对象,而是返回代理对象。

代理对象的生成时机:需要在 Bean 初始化完成后(即 postProcessAfterInitialization 阶段)。

(2)第三级缓存的作用
三级缓存存储的是 ObjectFactory,它可以在需要时生成代理对象。

流程示例:

实例化 A(原始对象)。

将 A 的 ObjectFactory 放入三级缓存(而非原始对象)。

注入 A 的依赖时,调用 ObjectFactory.getObject():

如果 A 需要代理,则返回代理对象;

如果不需要代理,则返回原始对象。

最终放入二级缓存的是 代理对象(或原始对象),而非直接暴露原始对象。

(3)如果没有三级缓存
如果直接将原始对象放入二级缓存,后续 AOP 代理无法替换它,导致 注入的是原始对象而非代理,可能引发问题(如事务失效)。


4. 哪些情况是三级缓存无法解决循环依赖?
场景原因
构造器注入(Constructor Injection)Spring 必须先完成构造器调用,无法提前暴露半成品 Bean。,可以使用@Lazy
原型(Prototype)作用域

Spring 不缓存原型 Bean,无法通过三级缓存机制解决。

提一点

对于原型对象,如果要解决循环依赖问题,要维护到底是哪两个对象之间的循环依赖,解决成本变高,而且循环依赖本来就不对,所以spring不支持。

如果检测到原型 Bean 的循环依赖,Spring 会直接报错:

org.springframework.beans.factory.BeanCurrentlyInCreationException: 
Error creating bean with name 'A': Requested bean is currently in creation: 
Is there an unresolvable circular reference?
@Async/@Transactional 等代理类

如果循环依赖涉及 AOP 代理,可能因代理生成时机(

1:代理对象生成时机冲突,注入的可能是原始对象而非代理

2:构造器注入 + AOP 代理    无法提前暴露半成品 Bean

3:原型 Bean + AOP 代理    原型 Bean 无法缓存半成品

问题导致失败(需用 @Lazy

一些极为特殊的情况。最好使用@Lazy,避免在复杂代理场景(如 @Transactional + @Async)中使用循环依赖


5. 如何避免或修复循环依赖?
(1) 代码设计层面
  • 避免双向依赖:重构代码,使用 单向依赖 或 接口隔离

  • 提取公共逻辑:将共用逻辑抽到第三个 Bean 中。

(2) Spring 提供的解决方案
  1. 使用 @Lazy 延迟加载
    在其中一个依赖上添加 @Lazy,让 Spring 暂时不注入真实对象,而是注入一个代理。

    @Service
    public class A {@Autowired@Lazy  // 延迟加载 Bprivate B b;
    }
  2. 改用 Setter/Field 注入
    替换构造器注入为属性注入:

    @Service
    public class A {private B b;@Autowired  // Setter 注入public void setB(B b) { this.b = b; }
    }
  3. 使用 ApplicationContext 手动获取 Bean
    在需要时再获取依赖(不推荐,破坏 IoC 设计):

    @Service
    public class A {@Autowiredprivate ApplicationContext context;public void doSomething() {B b = context.getBean(B.class); // 使用时再获取}
    }

6. 总结
要点说明
可解决的循环依赖单例 Bean + 属性注入(@Autowired)
Spring不可自动解决的循环依赖构造器注入、原型 Bean、某些 AOP 代理场景
解决方案@Lazy、Setter 注入、代码重构
Spring 底层机制三级缓存(Singleton Objects、Early Singleton Objects、Singleton Factories)

最佳实践

  • 优先通过 代码设计 避免循环依赖。

  • 必要时使用 @Lazy 或调整注入方式。

  • 避免在复杂项目中滥用循环依赖,降低维护成本。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词