面试必问:聊一聊Spring中bean的循环依赖问题 ?——从原理到避坑

聊一聊Spring中bean的循环依赖问题 ?

我的回答: 嗯,好的,首先我来解释一下循环依赖。

循环依赖其实就是循环引用,也就是两个或两个以上的bean互相持有对方,最终形成闭环,比如A依赖于B,B依赖于A。

循环依赖在spring中是允许存在,spring框架通过内部的三级缓存来解决了大部分的循环依赖问题。三级缓存,每一级缓存的作用如下:

①一级缓存:singletonObjects 单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象

②二级缓存:earlySingletonObjects 缓存早期的bean对象(生命周期还没走完,半成品的bean)

③三级缓存:singletonFactories 缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的(比如创建代理对象)

具体的流程大概是这个样子的 :

第一,先实例A对象,同时会创建ObjectFactory对象存入三级缓存singletonFactories

第二,A在初始化的时候需要B对象,这个走B的创建的逻辑

第三,B实例化完成,也会创建ObjectFactory对象存入三级缓存singletonFactories

第四,B需要注入A,通过三级缓存中获取ObjectFactory来生成一个A的对象同时存入二级缓存,这个是有两种情况,一个是可能是A的普通对象,另外一个是A的代理对象,都可以让ObjectFactory来生产对应的对象,这也是三级缓存的关键

第五,B通过从通过二级缓存earlySingletonObjects 获得到A的对象后可以正常注入,B创建成功,存入一级缓存singletonObjects

第六,回到A对象初始化,因为B对象已经创建完成,则可以直接注入B,A创建成功存入一次缓存singletonObjects

第七,二级缓存中的临时对象A清除

觉得有用的话,点赞收藏就是对硬核干货最好的认可~

> 接下来我们来详细了解一下这个问题

一、循环依赖:Spring Bean 的「死锁迷局」

1.1 什么是循环依赖?

在 Spring 的 Bean 世界里,循环依赖就像是一场复杂的 "死锁迷局",让 Spring 容器在初始化时陷入困境。简单来说,当两个或多个 Bean 形成依赖闭环时,就会出现循环依赖的情况。比如,Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,这样就形成了一个无法解开的依赖循环🔄

最常见的循环依赖场景是通过字段注入(@Autowired)或 Setter 注入实现的。假设我们有两个服务类,AService和BService:

less 复制代码
@Service
public class AService {
    @Autowired
    private BService bService;
}
@Service
public class BService {
    @Autowired
    private AService aService;
}

在这个例子中,AService依赖BService,而BService又依赖AService,形成了一个典型的双向依赖循环。Spring 容器在初始化AService时,会发现它依赖BService,于是尝试去创建BService;而在创建BService时,又发现它依赖AService,这样就陷入了一个递归创建的死循环,就像两个人互相拉着对方的手,谁也无法先迈出第一步🤯

还有一种比较隐蔽的循环依赖场景是三者或更多 Bean 之间的依赖循环。例如,Bean A 依赖 Bean B,Bean B 依赖 Bean C,而 Bean C 又依赖 Bean A,这种情况下的循环依赖更难被发现和调试。

值得注意的是,通过构造器注入的循环依赖会直接导致 Spring 容器启动失败。因为构造器注入是在 Bean 实例化时就需要完成依赖注入,而此时依赖的 Bean 还未创建,所以无法解决这种循环依赖。如下所示:

kotlin 复制代码
@Service
public class AService {
    private final BService bService;
    public AService(BService bService) {
        this.bService = bService;
    }
}
@Service
public class BService {
    private final AService aService;
    public BService(AService aService) {
        this.aService = aService;
    }
}

上述代码中,AService和BService通过构造器相互依赖,Spring 容器在启动时会抛出BeanCurrentlyInCreationException异常,提示存在无法解决的循环引用。

1.2 为什么会触发面试高频追问?

在 Spring 面试中,循环依赖问题几乎是一个必问的高频问题,这背后有着深层次的考察目的。

首先,循环依赖问题涉及到 Spring Bean 的生命周期。Spring Bean 的生命周期包括实例化、属性填充、初始化等多个阶段。了解循环依赖的原理,需要对这些阶段有清晰的认识。例如,在解决循环依赖时,Spring 利用了在实例化后、属性填充前提前暴露 Bean 的早期引用这一特性,这就要求面试者对 Bean 生命周期的各个阶段非常熟悉,明白在哪个阶段可以进行哪些操作,以及这些操作对解决循环依赖的作用。

其次,循环依赖的解决依赖于 Spring 的三级缓存机制。三级缓存机制是 Spring 解决循环依赖的核心,它涉及到三个缓存:singletonObjects(一级缓存)、earlySingletonObjects(二级缓存)和singletonFactories(三级缓存)。面试者需要深入理解这三个缓存的作用和工作原理,明白为什么需要三级缓存,而不是两级或一级缓存。例如,在存在 AOP 的情况下,三级缓存中的ObjectFactory可以延迟创建代理对象,确保在循环依赖场景下,注入的是正确的代理对象,而不是原始对象,这是理解三级缓存机制的关键所在🧐

循环依赖问题还可以考察面试者对不同依赖注入方式的理解。如前文所述,构造器注入和 Setter 注入在处理循环依赖时有着不同的表现。构造器注入的循环依赖无法解决,而 Setter 注入的循环依赖在 Spring 的支持下可以得到处理。面试者需要清楚地知道这种差异,并能解释其中的原因,这有助于面试官了解面试者对依赖注入机制的掌握程度。

Spring 循环依赖问题不仅仅是一个简单的技术问题,它背后涉及到 Spring 的核心原理和机制,通过对这个问题的追问,面试官可以全面考察面试者对 Spring 框架的理解和掌握程度。

二、Spring 的破局之道:三级缓存机制深度解析

面对循环依赖这一棘手问题,Spring 展现了其强大的设计智慧,通过独特的三级缓存机制巧妙地化解了这一难题。这一机制不仅是 Spring 面试中的高频考点,更是理解 Spring 框架底层原理的关键所在。

2.1 三级缓存的核心作用

Spring 的三级缓存机制,犹如一套精密的齿轮系统,每个齿轮都在解决循环依赖的过程中发挥着不可或缺的作用。这三级缓存分别是singletonObjects(一级缓存)、earlySingletonObjects(二级缓存)和singletonFactories(三级缓存),它们各自有着明确的职责和分工,共同协作完成了 Bean 的创建和循环依赖的解决。

缓存类型 数据结构 存储内容 访问顺序
一级缓存 singletonObjects 完全初始化的单例 Bean(成品) 优先读取
二级缓存 earlySingletonObjects 提前暴露的未完全初始化 Bean(半成品) 其次读取
三级缓存 singletonFactories Bean 工厂对象(用于生成半成品 Bean) 最后读取

一级缓存singletonObjects就像是一个成品仓库,存放着已经完全初始化好的单例 Bean,这些 Bean 可以直接被应用程序使用。当 Spring 容器需要获取一个 Bean 时,会首先从一级缓存中查找,如果找到了,就直接返回,这大大提高了 Bean 的获取效率。

二级缓存earlySingletonObjects则是一个半成品仓库,存放着提前暴露的未完全初始化的 Bean。这些 Bean 虽然还没有完成所有的初始化步骤,但已经可以被其他 Bean 引用,为解决循环依赖提供了关键的早期引用。

三级缓存singletonFactories中存储的是 Bean 工厂对象,这些工厂对象就像是生产 Bean 的机器,用于生成提前暴露的 Bean。在存在 AOP 代理的情况下,三级缓存尤为重要,它可以动态决定返回原始对象还是代理对象,确保在循环依赖场景下,注入的是正确的对象。

2.2 核心解决流程(以 A→B→A 为例)

Spring 使用三级缓存解决循环依赖的过程就像一场精心编排的舞蹈,每一个步骤都紧密相连,有条不紊。以 Bean A 依赖 Bean B,而 Bean B 又依赖 Bean A 的场景为例,其解决流程如下:

  1. 实例化 A:Spring 容器开始创建 Bean A,首先调用 A 的构造器创建一个原始对象。此时的 A 就像一个刚刚搭建好框架的房子,还没有进行内部装修和布置家具。接着,Spring 将 A 的工厂对象(ObjectFactory)存入三级缓存singletonFactories中。这个工厂对象就像是一个建筑承包商,负责后续对 A 的进一步加工和完善。
scss 复制代码
// 实例化A
BeanA a = new BeanA(); 
// 将A的工厂对象存入三级缓存
singletonFactories.put("a", () -> getEarlyBeanReference("a", mbd, a)); 
  1. 填充 A 的属性:在实例化 A 之后,Spring 开始为 A 填充属性。这时发现 A 依赖 Bean B,于是暂停 A 的创建,转而去创建 B。就好像在装修房子时,发现需要购买一些家具(Bean B),于是先放下手头的装修工作,去采购家具。
  1. 实例化 B:Spring 开始创建 Bean B,同样调用 B 的构造器创建一个原始对象,并将 B 的工厂对象存入三级缓存singletonFactories中。此时 B 也像一个刚刚搭建好框架的房子,等待着进一步的装修和布置。
scss 复制代码
// 实例化B
BeanB b = new BeanB(); 
// 将B的工厂对象存入三级缓存
singletonFactories.put("b", () -> getEarlyBeanReference("b", mbd, b)); 
  1. 填充 B 的属性:在实例化 B 之后,Spring 开始为 B 填充属性。这时发现 B 依赖 Bean A,于是从三级缓存singletonFactories中获取 A 的工厂对象。通过这个工厂对象生成 A 的早期引用(半成品 A),并将其转入二级缓存earlySingletonObjects中,同时从三级缓存中移除 A 的工厂对象。然后将这个早期引用的 A 注入到 B 中。这就好比在布置 B 这个房子的家具时,需要用到 A 这个房子的一些物品,于是从承包商那里拿到了 A 房子的部分物品(早期引用的 A),并将其布置到 B 房子中。
css 复制代码
// 从三级缓存获取A的工厂对象
ObjectFactory<?> singletonFactory = singletonFactories.get("a"); 
// 通过工厂对象获取A的早期引用
Object earlyA = singletonFactory.getObject(); 
// 将A的早期引用放入二级缓存
earlySingletonObjects.put("a", earlyA); 
// 从三级缓存移除A的工厂对象
singletonFactories.remove("a"); 
// 将早期引用的A注入到B中
b.setA(earlyA); 
  1. 完成 B 的初始化:在将早期引用的 A 注入到 B 中后,B 完成了属性填充和初始化,Spring 将 B 存入一级缓存singletonObjects中。此时 B 这个房子已经装修布置完毕,可以正式入住(被应用程序使用)。
css 复制代码
// 将B存入一级缓存
singletonObjects.put("b", b); 
  1. 完成 A 的初始化:B 完成初始化并存入一级缓存后,Spring 继续完成 A 的初始化。从一级缓存中获取已经初始化好的 B,注入到 A 中,完成 A 的属性填充和初始化。最后将 A 从二级缓存移至一级缓存。此时 A 这个房子也装修布置完毕,正式入住(被应用程序使用)。
css 复制代码
// 从一级缓存获取B
BeanB bFromCache = singletonObjects.get("b"); 
// 将B注入到A中
a.setB(bFromCache); 
// 将A从二级缓存移至一级缓存
singletonObjects.put("a", a); 
earlySingletonObjects.remove("a"); 

2.3 三级缓存的必要性:AOP 代理的关键作用

在理解了三级缓存的核心作用和解决循环依赖的流程后,我们不禁要问:为什么 Spring 需要三级缓存,而不是两级或一级缓存呢?这其中的关键在于 AOP 代理的处理。

在 Spring 中,当一个 Bean 需要被 AOP 增强时(例如使用了@Transactional注解开启事务),Spring 会在初始化阶段生成代理对象。这个代理对象就像是给原始对象穿上了一层特殊的 "外衣",使其具备了额外的功能(如事务管理、日志记录等)。如果只有两级缓存(一级缓存存放成品 Bean,二级缓存存放半成品 Bean),在填充属性时,注入的将是原始对象,而不是代理对象。这就好比给一个人穿上了普通的衣服,而不是他需要的特殊 "外衣",最终会导致注入的对象与实际需要的对象不一致,引发运行时异常。

而三级缓存的存在,通过ObjectFactory延迟代理生成,巧妙地解决了这个问题。在存在循环依赖时,三级缓存中的工厂对象会根据实际情况动态生成代理对象,确保注入的是最终形态的 Bean。例如,当 A 被 AOP 增强时,工厂会生成代理对象并返回,这样在整个循环依赖的解决过程中,无论是 B 对 A 的依赖注入,还是最终 A 的初始化,使用的都是同一个代理对象,保证了对象的一致性和正确性。

Spring 的三级缓存机制通过巧妙的设计,不仅成功解决了循环依赖问题,还兼顾了 AOP 代理的处理,确保了 Spring 容器中 Bean 的正确创建和初始化,是 Spring 框架设计的精妙之处。

三、三类无法解决的循环依赖场景

虽然 Spring 的三级缓存机制在解决循环依赖问题上表现出色,但并非所有的循环依赖场景都能被妥善处理。在实际应用中,有三类循环依赖场景是 Spring 无法解决的,了解这些场景对于我们正确使用 Spring 框架、避免潜在的问题至关重要。

3.1 构造器注入的循环依赖

构造器注入是一种在 Bean 实例化时就完成依赖注入的方式,这就要求依赖的 Bean 必须在构造器调用之前就已经完全初始化。当出现构造器注入的循环依赖时,Spring 容器在创建 Bean 时会陷入困境。以AService和BService为例:

kotlin 复制代码
@Service
public class AService {
    private final BService bService;
    public AService(BService bService) {
        this.bService = bService;
    }
}
@Service
public class BService {
    private final AService aService;
    public BService(AService aService) {
        this.aService = aService;
    }
}

在上述代码中,AService通过构造器依赖BService,而BService又通过构造器依赖AService。Spring 容器在创建AService时,需要先创建BService,而创建BService又需要先创建AService,这样就形成了一个无法打破的僵局。Spring 无法提前暴露未初始化的 Bean,因此直接抛出BeanCurrentlyInCreationException异常,提示存在无法解决的循环引用。

3.2 多例 Bean(prototype 作用域)的循环依赖

在 Spring 中,多例 Bean(prototype作用域)与单例 Bean 有着不同的生命周期管理方式。单例 Bean 在容器启动时创建,并在整个容器生命周期内保持唯一,而多例 Bean 则是每次被请求时都会创建一个新的实例。这一特性使得 Spring 无法为多例 Bean 维护缓存,也就无法利用三级缓存机制来解决循环依赖问题。

当多例 Bean 之间出现循环依赖时,每次创建新实例都会触发递归依赖。例如:

less 复制代码
@Scope("prototype")
@Service
public class AService {
    @Autowired
    private BService bService;
}
@Scope("prototype")
@Service
public class BService {
    @Autowired
    private AService aService;
}

在上述代码中,AService和BService都是多例 Bean,并且相互依赖。当 Spring 容器尝试创建AService时,会发现它依赖BService,于是去创建BService;而创建BService时,又发现它依赖AService,这样就会陷入一个无限递归的创建过程,导致容器无法完成初始化,最终抛出异常。

3.3 跨容器的循环依赖

在一些复杂的企业级应用中,可能会存在多个 Spring 容器,例如在使用 Spring Cloud 等微服务框架时,每个微服务都有自己独立的 Spring 容器。当出现跨容器的 Bean 引用时,如果这些引用形成了循环依赖,Spring 的本地缓存机制将无法解决。

假设我们有两个 Spring 容器ContainerA和ContainerB,ContainerA中的AService依赖ContainerB中的BService,而ContainerB中的BService又依赖ContainerA中的AService。由于两个容器之间相互独立,无法共享缓存,因此无法通过本地缓存机制来解决这种循环依赖。要解决跨容器的循环依赖,通常需要借助全局注册中心或中间层解耦,通过引入一个中间层组件来管理和协调不同容器之间的 Bean 引用,从而打破循环依赖。

四、实战避坑:从编码到调优的最佳实践

4.1 依赖注入方式选择

在实际开发中,依赖注入方式的选择对解决循环依赖问题至关重要。优先考虑 Setter 注入或字段注入,因为 Spring 的三级缓存机制对这种注入方式友好,能够有效解决循环依赖问题。例如:

typescript 复制代码
@Service
public class AService {
    private BService bService;
    @Autowired
    public void setBService(BService bService) {
        this.bService = bService;
    }
}
@Service
public class BService {
    private AService aService;
    @Autowired
    public void setAService(AService aService) {
        this.aService = aService;
    }
}

上述代码中,AService和BService通过 Setter 注入相互依赖,Spring 能够利用三级缓存机制顺利完成 Bean 的创建和依赖注入。

而构造器注入仅用于必需依赖的场景,避免在非必要情况下形成强依赖闭环。如果必须使用构造器注入,且存在循环依赖的可能,可以考虑结合@Lazy注解来打破循环,这在后续的@Lazy注解部分会详细介绍。

4.2 @Lazy 注解打破僵局

@Lazy注解是解决循环依赖的一把利器,尤其是在构造器注入的循环依赖场景中。它的核心原理是延迟 Bean 的初始化,将依赖解析推迟到首次使用时。例如,当AService和BService通过构造器相互依赖时:

kotlin 复制代码
@Service
public class AService {
    private final BService bService;
    public AService(@Lazy BService bService) {
        this.bService = bService;
    }
}
@Service
public class BService {
    private final AService aService;
    public BService(AService aService) {
        this.aService = aService;
    }
}

在上述代码中,AService的构造器依赖BService,通过@Lazy注解,Spring 在创建AService时,不会立即创建BService,而是注入一个代理对象作为占位符。当AService首次使用bService时,才会触发BService的创建,从而打破了构造器注入的循环依赖。

@Lazy注解不仅适用于构造器注入,在 Setter 注入或字段注入的复杂场景中,也可以作为一种明确的延迟加载手段,进一步优化系统性能和资源使用。

4.3 架构层面解耦

从架构层面进行解耦是解决循环依赖的根本之道,它能够从源头上避免循环依赖的产生,同时提升系统的可维护性和可扩展性。

引入中间层是一种常见的解耦策略。例如,当AService和BService相互依赖时,可以将它们共同依赖的功能提取出来,创建一个独立的CService:

kotlin 复制代码
@Service
public class AService {
    private final CService cService;
    public AService(CService cService) {
        this.cService = cService;
    }
}
@Service
public class BService {
    private final CService cService;
    public BService(CService cService) {
        this.cService = cService;
    }
}
@Service
public class CService {
    // 公共功能实现
}

通过引入CService,AService和BService不再直接相互依赖,而是依赖于中间层CService,从而打破了循环依赖,降低了耦合度。

遵循依赖倒置原则也是解耦的关键。通过接口解耦,高层模块不直接依赖低层模块的实现,而是依赖于抽象接口。例如:

java 复制代码
public interface IService {
    void execute();
}
@Service
public class AServiceImpl implements IService {
    private final BService bService;
    public AServiceImpl(BService bService) {
        this.bService = bService;
    }
    @Override
    public void execute() {
        // 业务逻辑
    }
}
@Service
public class BService {
    private final IService aService;
    public BService(IService aService) {
        this.aService = aService;
    }
}

在上述代码中,AServiceImpl实现了IService接口,BService依赖于IService接口,而不是具体的AServiceImpl类。这样,当业务逻辑发生变化时,只需要修改具体的实现类,而不会影响到依赖它的其他模块,提高了系统的灵活性和可维护性。

4.4 调试工具推荐

在开发过程中,及时发现和解决循环依赖问题离不开有效的调试工具。Spring Boot Actuator 和 IDE 的依赖分析功能为我们提供了强大的支持。

Spring Boot Actuator 是 Spring Boot 提供的一个监控和管理生产环境应用的模块。通过它的/beans端点,我们可以查看 Spring 容器中所有 Bean 的详细信息,包括依赖关系。例如,启动应用后,访问http://localhost:8080/actuator/beans,可以看到类似如下的信息:

json 复制代码
{
    "contexts": {
        "application": {
            "beans": {
                "aService": {
                    "aliases": [],
                    "scope": "singleton",
                    "type": "com.example.demo.AService",
                    "resource": "class path resource [com/example/demo/AService.class]",
                    "dependencies": ["bService"]
                },
                "bService": {
                    "aliases": [],
                    "scope": "singleton",
                    "type": "com.example.demo.BService",
                    "resource": "class path resource [com/example/demo/BService.class]",
                    "dependencies": ["aService"]
                }
            }
        }
    }
}

从上述信息中,可以清晰地看到AService依赖于BService,BService依赖于AService,从而快速定位到循环依赖的问题。

IDE 的依赖分析功能也非常强大,以 IntelliJ IDEA 为例,它的 Diagram 功能可以可视化 Bean 的依赖关系。通过右键点击项目中的类,选择Diagram -> Show Diagram,可以生成类的依赖图。在依赖图中,循环依赖的关系会以直观的方式呈现出来,方便我们进行分析和调试。

五、面试陷阱:这些细节决定你是否「懂原理」

5.1 二级缓存能否替代三级缓存?

在面试中,关于二级缓存能否替代三级缓存的问题常常出现,这背后涉及到 Spring 解决循环依赖机制的核心原理。

在无 AOP 场景下,仅使用二级缓存理论上可以解决循环依赖问题。二级缓存earlySingletonObjects能够存储提前暴露的未完全初始化的 Bean,在循环依赖场景中,当 Bean A 依赖 Bean B,而 Bean B 又依赖 Bean A 时,二级缓存可以提供早期引用,打破循环。例如,在一个简单的业务场景中,AService和BService相互依赖:

less 复制代码
@Service
public class AService {
    @Autowired
    private BService bService;
}
@Service
public class BService {
    @Autowired
    private AService aService;
}

在这种情况下,二级缓存可以在 Bean A 实例化后,将其放入二级缓存,当 Bean B 创建并需要依赖 Bean A 时,可以从二级缓存中获取 Bean A 的早期引用,从而完成 Bean B 的创建,进而完成 Bean A 的创建。

然而,在有 AOP 场景下,二级缓存就无法满足需求了。当 Bean 需要被 AOP 增强时,Spring 会在初始化阶段生成代理对象。如果只有二级缓存,在填充属性时,注入的将是原始对象,而不是代理对象,这会导致注入的对象与最终的代理对象不一致,引发业务逻辑错误。例如,当AService使用@Transactional注解开启事务时,需要生成代理对象来实现事务管理功能:

less 复制代码
@Service
@Transactional
public class AService {
    @Autowired
    private BService bService;
}
@Service
public class BService {
    @Autowired
    private AService aService;
}

在这种情况下,三级缓存中的ObjectFactory就发挥了关键作用。ObjectFactory可以延迟生成代理对象,当发生循环依赖时,通过工厂的getObject()方法判断是否需要创建代理对象,确保注入的是正确的代理实例,从而保证了依赖注入的对象与最终的代理对象一致。

5.2 循环依赖对 Bean 生命周期的影响

循环依赖对 Bean 生命周期的影响也是面试中容易被问到的细节。虽然 Spring 通过三级缓存机制解决了循环依赖问题,但这也会对 Bean 的生命周期产生一些特殊的影响。

在正常情况下,Spring Bean 的生命周期包括实例化、属性填充、初始化等多个阶段。在解决循环依赖时,Spring 会在 Bean 实例化后,属性填充前提前暴露 Bean 的早期引用。这意味着,提前暴露的 Bean 在生命周期上是不完整的,它跳过了部分初始化步骤,如后置处理器的调用等。例如,一个 Bean 实现了InitializingBean接口或使用了@PostConstruct注解,在提前暴露时,这些初始化方法还未执行。

虽然提前暴露的 Bean 在生命周期上不完整,但这并不意味着它们不能正常使用。在 Spring 的设计中,通过三级缓存机制,最终会将完整初始化的 Bean 存入一级缓存singletonObjects中,应用程序在使用 Bean 时,获取到的是完整初始化的 Bean。不过,在多线程环境下,需要注意提前暴露的半成品 Bean 的线程安全问题,因为它们可能在未完全初始化的情况下被多个线程访问。

5.3 Spring 6.x 的优化点

Spring 6.x 在解决循环依赖问题上进行了一些优化,这些优化点也是面试中可能会涉及到的内容。

在最新的 Spring 6.x 版本中,增强了循环依赖检测逻辑,在解析阶段提前预警潜在问题。通过在早期解析 Bean 定义时,对依赖关系进行更深入的分析,Spring 可以在启动前就发现可能存在的循环依赖,而不是等到创建 Bean 时才抛出异常。这大大减少了启动时的异常,提高了系统的稳定性和可维护性。例如,在配置文件中定义了相互依赖的 Bean 时,Spring 6.x 可以在启动前就检测到这种循环依赖,并给出明确的提示,帮助开发者及时发现和解决问题。

Spring 6.x 还对缓存机制进行了一些性能优化,提高了循环依赖场景下 Bean 的创建效率。通过更合理的缓存管理和对象创建策略,减少了不必要的对象创建和缓存操作,从而提升了系统的性能。

总结:从面试考点到生产实践的完整链路

理解 Spring 循环依赖的本质是掌握 Bean 生命周期与缓存机制的协同工作。面对面试,需清晰区分不同场景的处理差异;在生产中,应通过合理的依赖设计避免问题,而非依赖框架特性解决。记住:三级缓存的核心不是「缓存」,而是通过提前暴露引用打破依赖闭环,这正是 Spring 框架设计的精妙之处。你在实际开发中遇到过哪些奇葩的循环依赖场景?欢迎在评论区分享你的解决方案~

相关推荐
最初的↘那颗心38 分钟前
Java 泛型类型擦除
java·flink
uhakadotcom1 小时前
使用postgresql时有哪些简单有用的最佳实践
后端·面试·github
IT毕设实战小研1 小时前
基于Spring Boot校园二手交易平台系统设计与实现 二手交易系统 交易平台小程序
java·数据库·vue.js·spring boot·后端·小程序·课程设计
bobz9651 小时前
QT 字体
后端
泉城老铁1 小时前
Spring Boot 中根据 Word 模板导出包含表格、图表等复杂格式的文档
java·后端
用户4099322502121 小时前
如何在FastAPI中玩转APScheduler,实现动态定时任务的魔法?
后端·github·trae
极客BIM工作室1 小时前
谈谈《More Effective C++》的条款30:代理类
java·开发语言·c++
孤狼程序员1 小时前
【Spring Cloud 微服务】1.Hystrix断路器
java·spring boot·spring·微服务
风象南1 小时前
开发者必备工具:用 SpringBoot 构建轻量级日志查看器,省时又省力
后端
RainbowSea2 小时前
伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 04
java·spring boot·后端