面试官:Spring是如何解决循依赖问题?

Spring 的循环依赖一直都是 Spring 中一个很重要的话题,一方面是 Spring 为了解决循环依赖做了很多工作,另一个方面是因为它是面试 Spring 的常客,因为他要求你看过 Spring 的源码,如果没有看过 Spring 源码你基本上是回答不了这个问题的,虽然也有一些面试经会分析这个问题,但是如果别人深究的话,你没有看过源码真心回答不了。

什么是循环依赖

从字面上来理解就是 Spring Bean 之间的依赖产生了循环,例如 A 依赖 B ,B 依赖 C,C 依赖 A,如下:

代码大致如下:

@Service
public class AService {
    @Autowired
    private BService bService;
}

@Service
public class BService {
    @Autowired
    private CService cService;
}

@Service
public class CService {
    @Autowired
    private AService aService;
}


Spring 解决了哪些情况的循环依赖

在文章Spring 中的 Bean 有几种作用域?,大明哥说到 Spring 有五种作用域:

  1. singleton:单例作用域
  2. prototype:原型作用域
  3. request:请求作用域
  4. session:会话作用域
  5. application:全局作用域

但是 Spring 只解决单例作用域(singleton)循环依赖,主要原因如下:

  • prototype:每次请求都会创建新的 Bean 实例,会形成一个死循环,同时用一次就丢,解决循环依赖成本比较大。
  • requestsessionapplication:解决他们的循环依赖需要很复杂的处理机制,可能会引入额外的性能开销和复杂性。
  • 同时 ,Spring 的设计哲学倾向于鼓励良好的编程实践,依赖循环依赖可能是设计上的问题,我们应该是采取更加简洁和更加优雅的代码结构,而不是依赖 Spring 来解决它。

同时,在文章 Spring为什么建议使用构造器来注入 中讲到,Spring 有三种注入方式:

  • 基于字段的注入
  • 基于 setter 方法的注入
  • 基于构造器注入

对于这三种注入方式 ,Spring 不解决全是基于构造器注入的方式 ,因为 Spring 会报错,例如我们将上面的调整为构造器注入,启动时会报如下错误 :

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  AService defined in file [/xxx/AService.class]
↑     ↓
|  BService defined in file [/xxx/BService.class]
↑     ↓
|  CService defined in file [/xxx/CService.class]
└─────┘

注:我是依赖 Spring Boot 写的测试案例

基于上面的分析,我们知道 Spring 解决循环依赖的前提条件是 :

  1. 出现循环依赖的 B 必须是单例作用域。
  2. 依赖注入的方式不能全是构造器注入的方式。

有很多文章说,Spring 只解决 setter 方法的循环依赖,这是错误的,我们来演示下:

@Service
public class AService {

    @Autowired
    private BService bService;
}

B、C 还是构造器注入,你会发现这样启动是不会报错的。

基于这个问题,大明哥留一个思考题,如果 A、B、C 的注入方式是如下:

@Service
public class AService {

    private BService bService;

    @Autowired
    public AService(BService bService) {
        this.bService = bService;
    }
}

@Service
public class BService {

    @Autowired
    private CService cService;
}

@Service
public class CService {

    private AService aService;

    @Autowired
    public CService(AService aService) {
        this.aService = aService;
    }
}

会报错吗?为什么?

Spring 是怎么解决循环依赖的

Spring 解决循环依赖就靠三招:

  1. 单例模式
  2. 三级缓存
  3. 提前暴露、早起引用

单例模式前面已经分析过了,我们先看三级缓存。Spring 提供了三级缓存用来存储单例 的 Bean 实例(下面所说的 Bean 全部都为单例模式的 Bean),这三个缓存是互斥的,同一个 Bean 实例只会三级缓存中的一个中存在。三级缓存分别是:

  • 一级缓存singletonObjects
    • 用于存放完全初始化好的 Bean。
    • 当一个 Bean 被完全初始化好后(即所有的属性都被注入,所有的初始化方法都被调用),它会被放入这个缓存中。在此之后,每次请求这个 Bean 时,Spring 容器都会直接从这个缓存返回实例。
  • 二级缓存earlySingletonObjects
    • 用于存放提前暴露的 Bean 对象,即已经实例化但尚未完全初始化(未完成依赖注入和初始化方法调用)的 Bean,注意该 Bean 还处于创建中。
    • 该缓存是解决循环依赖的核心所在。当一个 Bean 正在创建的过程中,如果另外一个 Bean 需要引用它,则 Spring 为它提供该 Bean的一个早期引用,这个早期引用就存放在 earlySingletonObjects 缓存中。
  • 三级缓存singletonFactories
    • 存放 Bean 的工厂对象,用于生成 Bean 的早期引用。
    • 它是解决循环依赖的第一步。当一个 Bean 开始创建时,Spring 首先在这个缓存中放入一个工厂对象。这个工厂对象能够生成 Bean 的早期引用,当这个 Bean 需要被注入到其他 Bean 中时,就会通过这个工厂对象来创建早期引用。

三个缓存协同工作 ,以确保在应用中存在循环依赖的情况下,Spring 容器依然可以正确创建 Bean,并管理他们。

现在跟着大明哥的脚步来详细分析 Spring 是如何解决循环依赖的,为了更好地演示,我们将上面三个 Bean 循环依赖调整为两个即 A 依赖 B,B 依赖 A。

Spring 创建 Bean 的过程分为三个步骤:

  1. 实例化:AbstractAutowireCapableBeanFactory#createBean()
  2. 属性注入:AbstractAutowireCapableBeanFactory#populateBean()
  3. 初始化:AbstractAutowireCapableBeanFactory#initializeBean()

首先我们创建 A,先进行 A 对象的实例化过程 ,跟踪 AbstractAutowireCapableBeanFactory#createBean(),到 doCreateBean()

    protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
        // .....

        boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
        if (earlySingletonExposure) {
            // 加入到三级缓存 singletonFactories 中
            addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }

        // ...
    }

earlySingletonExposuretrue,则将 singletonFactory 保存到三级缓存 singletonFactories 中。条件为:

  • mbd.isSingleton():为单例模式
  • this.allowCircularReferences:检查配置是否允许循环引用
  • this.isSingletonCurrentlyInCreation(beanName):检查当前 Bean 是否正在创建。这是为了检测 Bean 是否处于创建的半成品状态(即已经开始创建但还没有完全初始化),这种状态的 Bean 是解决循环依赖的关键,因为他需要提前暴露给其他 Bean 引用,用来解决循环依赖 。

addSingletonFactory() 是将 singletonFactory 添加到三级缓存中:

  protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
      if (!this.singletonObjects.containsKey(beanName)) {
        // 添加到三级缓存中
        this.singletonFactories.put(beanName, singletonFactory);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
      }
    }
  }

singletonFactory 由 getEarlyBeanReference() 创建:

  protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
      for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
        exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
      }
    }
    return exposedObject;
  }

这个方法是一个很重要的方法,用来出来 AOP 的,我们先放在这里,后面来分析。

A 实例化后,就调用 populateBean() 来完成属性注入,这里开始注入 B,调用 getBean(b) ,我们一直跟踪源代码到 getSingleton(String beanName, boolean allowEarlyReference),该方法有两个参数:

  • beanName:获取 Bean 实例的名称。
  • allowEarlyReference:用于指定是否允许早期引用。
    • true:允许在 Bean 的初始化过程中提前获取引用,即使 Bean 正在创建中。
    • false:只有在 Bean 创建完成后才能获取引用

代码如下:

  @Nullable
  protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 从一级缓存中获取完整的 Bean
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果 singletonObject 为空,且在新建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      // 从二级缓存中获取该 Bean 的早期引用
      singletonObject = this.earlySingletonObjects.get(beanName);
      // 早期引用为空,且允许早期引用
      if (singletonObject == null && allowEarlyReference) {
        synchronized (this.singletonObjects) {
          // 双重检查
          // 一级
          singletonObject = this.singletonObjects.get(beanName);
          if (singletonObject == null) {
            // 二级
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null) {
              // 三级
              ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
              if (singletonFactory != null) {
                // 从单例工厂获取单例 Bean
                // 注意这个 Bean 还只是一个早期引用
                singletonObject = singletonFactory.getObject();
                
                // 将 Bean 从三级缓存移动到二级缓存去
                this.earlySingletonObjects.put(beanName, singletonObject);
                this.singletonFactories.remove(beanName);
              }
            }
          }
        }
      }
    }
    return singletonObject;
  }

由于 B 还没有创建,所以这里会返回 null 。故 B 依然和 A 一样去走创建过程,也会在三级缓存中存放 Bean 工厂。当 B 完成实例化后开始属性注入,这个时候它会调用 getBean(a) 去获取 A 的实例对象,由于 A 还处于创建过程中,一级缓存没有,二级缓存也没有,但是在三级缓存 A 提前暴露了一个 Bean 工厂对象,B 可以在三级缓存中获取 A 的 Bean 工厂对象,通过 singletonFactory.getObject() 获取 A 的早期引用,完成注入,也就是这段代码:

// 从单例工厂获取单例 Bean
// 注意这个 Bean 还只是一个早期引用
singletonObject = singletonFactory.getObject();
                
// 将 Bean 从三级缓存移动到二级缓存去
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);

B 完成注入后,开始初始化,最终得到一个完整 B。B 完成后,这个时候 A 得到的就是一个完整的 B 实例对象,A 完成注入,并进行初始化。整个过程如下图:

到这里循环依赖已经解决了。整个过程这张图已经详细阐述了,大明哥就不过多阐述了。在上面大明哥还埋了一个点,就是 getEarlyBeanReference(),我们继续。

我们知道注入到 B 中的 A 是通过 getEarlyBeanReference() 提前暴露出去的一个对象获取的,我们再看这个方法:

  protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
      for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
        exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
      }
    }
    return exposedObject;
  }

这里的参数 bean 就是已经实例化的 A 对象。

这行代码是关键:if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()),如果它为 false,这个方法等同于:

  protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    return exposedObject;
  }

返回的是一个原生的 A 对象,但是如果 if 语句为 true 呢?那么 exposedObject = bp.getEarlyBeanReference(exposedObject, beanName),调用后置处理器的 getEarlyBeanReference() ,然后真正实现这个方法的后置处理器就只有一个地方,那就是 AOP 的 AnnotationAwareAspectJAutoProxyCreator,如果我们的 A 进行了 AOP 代理 ,那么注入 B 的是 A 的代理对象而不是 A 本身。

<think>嗯,用户问的是如何学习Spring,我得先理清楚学习Spring的步骤,确保覆盖到关键点。首先,用户可能是刚开始学Java,或者已经有Java基础想进阶框架,需要明确学习Spring的前提条件。 Spring框架内容很多,可能会让新手感到困惑,所以应该分阶段来。先学核心概念,比如IoCAOP,这两个是Spring的基础。然后可能提到Spring MVC,Spring Boot,这些是现代开发常用的,尤其是Boot简化了配置,适合快速上手。 用户可能需要实际操作的例子,所以推荐结合教程项目实践。比如官方文档Spring Guides里的示例,这样用户可以边学边做。另外,常见的面试题如Bean的作用域、事务管理,这些也是重点,需要提到。 可能用户对Spring的不同模块不太清楚,要区分Spring Framework、Spring Boot、Spring Cloud等,解释各自用途。同时,强调社区资源,比如论坛、Stack Overflow,遇到问题如何寻求帮助。 还要注意版本问题Spring 5Spring 6的新特性,比如响应式编程,但如果是初学者,可能先打好基础再学这些。另外,整合其他技术如JPA、MyBatis,或者安全框架Spring Security,这些也是实际项目需要的,可以放在中高级阶段。 测试部分也很重要,Spring的测试支持,比如JUnit 5整合,确保代码质量。最后,持续学习,跟进官方更新,参与开源项目,这些建议能帮助用户不断提升。 总结下来,结构应该是先打基础,再学具体模块,结合实践理论,利用好资源,逐步深入。避免信息过载,分步骤引导用户,确保他们能跟上节奏,并应用到实际项目中。</think>学习Spring框架需要序渐进地掌握其核心概念生态体系。以下是为您整理的系统学习路径: 一、基础准备阶段 1. Java基础要求 - 必须掌握:面向对象编程、集合框架、IO流、多线程 - 推荐掌握:Java8+特性(Lambda、Stream API) - 环境准备:JDK 11+、Maven/Gradle、IDE(推荐IntelliJ IDEA) 二、Spring Framework核心 1. IoC容器 - 理解控制反转原理:$$ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); $$ - 掌握Bean管理:作用域(singleton/prototype)、生命周期回调 - 依赖注入方式:构造器注入、Setter注入、@Autowired注解 2. AOP编程 - 核心概念:切面(Aspect)、连接点(JoinPoint)、通知(Advice) - 实现方式:XML配置 vs 注解方式 - 典型应用:日志记录、事务管理 三、Spring Boot入门 1. 快速启动 - 使用Spring Initializr生成项目:$ https://start.spring.io/ $ - 核心注解:@SpringBootApplication - 自动配置原理:spring-boot-autoconfigure模块 2. Web开发 - RESTful接口开发:@RestController - 请求映射:@GetMapping/@PostMapping - 参数接收:@RequestParam、@PathVariable 四、实践提升路径 1. 推荐学习顺序 - Spring Core → Spring MVC → Spring Boot → Spring Data → Spring Security 2. 项目实践建议 ```java // 示例:简单的REST控制器 @RestController public class DemoController { @GetMapping("/hello") public String hello(@RequestParam String name) { return "Hello " + name; } } ``` 五、关键学习资源 1. 官方文档(优先阅读) - Spring Framework:$ https://spring.io/projects/spring-framework $ - Spring Boot:$ https://spring.io/projects/spring-boot $ 2. 推荐书籍 - 《Spring实战(第6版)》 - 《Spring Boot编程思想(核心篇)》 六、常见面试重点 1. 高频考点 - Bean生命周期 - 事务传播机制 - Spring Boot自动配置原理 - Spring MVC处理流程 七、学习建议 1. 实践策略 - 从官方Spring Guides案例入手 - 使用Spring Data JPA实现CRUD操作 - 尝试整合Redis、RabbitMQ等中间件 2. 调试技巧 - 启用调试日志:$ logging.level.root=DEBUG $ - 使用Actuator端点监控应用状态 八、生态扩展 1. 微服务方向 - Spring Cloud Netflix(Eureka/Hystrix) - Spring Cloud Alibaba(Nacos/Sentinel) 2. 响应式编程 - WebFlux模块 - Reactor编程模型 建议每天保持2-3小时的实践编码,结合官方文档逐步深入。遇到问题时,优先查阅Spring官方Issue列表Stack Overflow的相关讨论。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大明哥_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值