第10篇 · IoC容器深度解密:Spring的“心脏”如何跳动

如果你是从第01篇一路看下来的,到这一篇,我们终于要触及 Spring 最核心的部分了。

前面我们讲了 Servlet 规范、JDBC 演进、MyBatis 原理------这些都是 Spring 生态中"被管理"的部分。但 Spring 本身是靠什么来"管理"它们的?答案是 IoC 容器

IoC 容器是 Spring 的"心脏"。你平时用的 @Autowired@Service@Component,本质上都是这个心脏在跳动时产生的现象。如果你不理解 IoC 容器的工作原理,那你对 Spring 的理解就停留在"会用注解"的层面------遇到循环依赖报错、依赖注入失败这类问题时,你就只能靠试错来排查。

这一篇,我们把 IoC 容器拆开来看。

学习目标

  • 理解 IoC(控制反转)的本质------将对象的创建和依赖关系的管理交给容器
  • 理解 DI(依赖注入)是 IoC 的具体实现方式
  • 掌握 BeanFactoryApplicationContext 的关系与区别
  • 理解 Spring 是如何通过 "三级缓存" 解决循环依赖的

正文

一、从"造车"案例理解 IoC:传统写法 vs IoC 写法

假设你要造一辆汽车。汽车依赖底盘,底盘依赖车轮。在传统编程中,你会这样写:

java 复制代码
// 传统写法:上层主动创建下层依赖
class Tire {
    // 车轮
}

class Bottom {
    private Tire tire = new Tire();  // 主动创建
}

class Framework {
    private Bottom bottom = new Bottom();  // 主动创建
}

class Car {
    private Framework framework = new Framework();  // 主动创建
}

每一层都"主动"去创建它依赖的对象。这种做法的核心问题是:Car 不仅要知道 Framework,还要知道 Bottom 和 Tire 的创建细节。依赖关系越深,耦合越紧。

IoC 的写法:把"创建对象"这件事交给容器,对象之间只声明"我需要什么",而不关心"怎么得到"。

java 复制代码
// IoC 写法:只声明依赖,不主动创建
class Tire { }

class Bottom {
    private Tire tire;
    public Bottom(Tire tire) {   // 依赖通过构造方法传入
        this.tire = tire;
    }
}

class Framework {
    private Bottom bottom;
    public Framework(Bottom bottom) {
        this.bottom = bottom;
    }
}

class Car {
    private Framework framework;
    public Car(Framework framework) {
        this.framework = framework;
    }
}

// 容器负责组装
Tire tire = new Tire();
Bottom bottom = new Bottom(tire);
Framework framework = new Framework(bottom);
Car car = new Car(framework);

在 IoC 写法中,控制权发生了反转:对象不再自己控制依赖的创建,而是把控制权交给外部(容器)。容器负责实例化所有对象,并根据依赖关系把它们组装起来。

这就是"控制反转"(Inversion of Control)这个名字的由来------控制权从对象本身反转到了容器

而"依赖注入"(Dependency Injection)是 IoC 的具体实现方式:容器把依赖"注入"到对象中,而不是由对象自己去"查找"或"创建"依赖。

二、BeanFactory vs ApplicationContext:容器的"基本款"和"全家桶"

Spring 的 IoC 容器有两个核心接口:BeanFactoryApplicationContext

BeanFactory 是 Spring IoC 容器的根接口 ,定义了容器最核心的能力:管理 Bean 的实例化、配置和依赖装配。它的最主要方法就是 getBean(String beanName)

ApplicationContextBeanFactory 的子接口 ,在 BeanFactory 的基础上增加了更多企业级功能:

功能 BeanFactory ApplicationContext
Bean 实例化与装配 ✅ 支持 ✅ 支持
集成生命周期管理
自动注册 BeanPostProcessor
自动注册 BeanFactoryPostProcessor
国际化(MessageSource)
事件发布机制(ApplicationEvent)

两者最核心的区别有两点

第一,Bean 的加载时机不同BeanFactory 采用延迟加载 ------只有在调用 getBean() 时才会创建 Bean 实例。而 ApplicationContext 在容器启动时就会完成所有 Bean 的初始化和装配。这意味着 ApplicationContext 启动稍慢,但你能在启动时就发现配置问题;BeanFactory 启动快,但问题可能会在运行时才暴露。

第二,功能完整度不同BeanFactory 只提供最基本的 IoC 功能。而 ApplicationContext 是一个"完整的超集",包含了 BeanFactory 的所有功能,还额外提供了 AOP 集成、事件发布、国际化等企业级特性。

实际开发中怎么选?

Spring 官方文档的建议非常明确:除非你有充分的理由不这样做,否则应该使用 ApplicationContext 。在绝大多数场景下------包括 Spring Boot 应用------你用的都是 ApplicationContext 的各种实现(如 AnnotationConfigApplicationContext)。

BeanFactory 的使用场景非常有限:比如在资源受限的环境(如移动设备)中,或者你需要对 Bean 的处理过程有完全控制时。对日常开发来说,记住一点就够了:你用的一直是 ApplicationContextBeanFactory 是它的底层基础

三、Bean 的存储方式:五大类注解 vs @Bean 方法注解

在 Spring 中,把对象交给容器管理有两种方式。

方式一:类注解( stereotype 注解)

在类上直接加注解,Spring 在扫描时会自动将这些类注册为 Bean:

java 复制代码
@Component  // 通用组件
@Service    // 业务层
@Repository // 数据访问层
@Controller // Web 层
@RestController // REST 控制器(@Controller + @ResponseBody)

这五个注解本质上是一样的------@Service@Repository@Controller 都是 @Component 的"别名",只是语义上区分了不同层次。Spring 在扫描时会把它们都当作 @Component 处理。

适用场景:你自己写的类,你能修改源码。

方式二:@Bean 方法注解

@Configuration 类中,通过 @Bean 方法显式声明 Bean:

java 复制代码
@Configuration
public class AppConfig {
    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }
}

适用场景:第三方库的类(你改不了源码)、需要复杂初始化逻辑的 Bean、或者你想在创建 Bean 时做一些额外配置。

两者怎么选? 很简单:自己写的类用注解,第三方的类用 @Bean

四、依赖注入的三种方式

Spring 支持三种依赖注入方式。

方式一:字段注入(Field Injection)------ 最常用,但有争议

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;  // 直接注入到字段
}

优点:写法最简单。缺点:字段是 private 的,Spring 通过反射强行注入------这破坏了封装性,也让单元测试变得困难(因为无法通过构造方法传入 Mock 对象)。

方式二:构造方法注入(Constructor Injection)------ 官方推荐

java 复制代码
@Service
public class UserService {
    private final UserMapper userMapper;
    
    public UserService(UserMapper userMapper) {  // 通过构造方法注入
        this.userMapper = userMapper;
    }
}

优点:依赖不可变(final 修饰),对象创建后依赖关系不会改变;构造方法执行时依赖已经被注入,不会出现 null 的情况;单元测试友好。这也是 Spring 官方推荐的方式。

从 Spring 4.3 开始,如果类只有一个构造方法,@Autowired 可以省略------Spring 会自动使用这个构造方法进行注入。

方式三:Setter 注入(Setter Injection)

java 复制代码
@Service
public class UserService {
    private UserMapper userMapper;
    
    @Autowired
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
}

优点:可以在对象创建后重新设置依赖。缺点:依赖可变,对象状态不够稳定。

选型建议

注入方式 推荐度 适用场景
构造方法注入 ⭐⭐⭐⭐⭐ 首选 绝大多数场景,尤其是强制性依赖
Setter 注入 ⭐⭐⭐ 可选 可选依赖,或需要在运行时重新注入的场景
字段注入 ⭐⭐ 谨慎使用 快速原型开发,但生产代码不推荐

Spring 官方推荐的原则是:强制性依赖用构造方法注入,可选依赖用 Setter 注入

五、循环依赖与三级缓存

这是 Spring 源码中最经典的问题,也是面试中的高频考点。

什么是循环依赖?

两个或多个 Bean 互相持有对方的引用,形成一个闭环。最典型的场景:

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

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

A 依赖 B,B 依赖 A------这就是循环依赖。如果 Spring 不做特殊处理,项目启动时会抛出 BeanCurrentlyInCreationException

Spring 如何解决?------ 三级缓存

Spring 通过三级缓存机制来解决单例 Bean 的循环依赖问题。这三个缓存分别是:

缓存级别 缓存名称 存储内容 作用
一级缓存 singletonObjects 完全初始化完成的 Bean(成品) 供业务直接使用
二级缓存 earlySingletonObjects 提前暴露的"半成品" Bean(已实例化,未完成属性填充) 在循环依赖中临时存放
三级缓存 singletonFactories ObjectFactory(对象工厂) 在需要时创建 Bean 实例并提前暴露

三级缓存的源码定义在 DefaultSingletonBeanRegistry 类中:

java 复制代码
public class DefaultSingletonBeanRegistry {
    // 一级缓存:成品 Bean
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    
    // 三级缓存:ObjectFactory
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
    
    // 二级缓存:半成品 Bean
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
    
    // 正在创建的 Bean 名称集合
    private final Set<String> singletonsCurrentlyInCreation = 
        Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}

三级缓存解决循环依赖的完整流程(以 A 依赖 B、B 依赖 A 为例):

  1. 创建 A:Spring 开始创建 A,调用构造方法实例化 A(得到一个"原始对象"),此时 A 的属性还未填充。
  2. A 放入三级缓存 :Spring 将 A 的 ObjectFactory 放入三级缓存 singletonFactories。这个 ObjectFactory 可以在需要时返回 A 的早期引用。
  3. 填充 A 的属性:Spring 开始为 A 填充属性,发现 A 依赖 B。
  4. 创建 B:Spring 开始创建 B,调用构造方法实例化 B。
  5. B 放入三级缓存 :Spring 将 B 的 ObjectFactory 放入三级缓存。
  6. 填充 B 的属性:Spring 开始为 B 填充属性,发现 B 依赖 A。
  7. B 从三级缓存获取 A :Spring 在缓存中查找 A------一级缓存没有(A 还没完成),二级缓存没有(A 还没被提前暴露)。于是从三级缓存中获取 A 的 ObjectFactory,调用 getObject() 得到 A 的早期引用。
  8. A 的早期引用放入二级缓存 :Spring 将 A 的早期引用放入二级缓存 earlySingletonObjects,并从三级缓存中移除 A 的 ObjectFactory
  9. B 完成创建 :B 的属性填充完成,B 成为一个完整的 Bean,放入一级缓存 singletonObjects
  10. A 继续完成创建:Spring 回到 A 的创建过程,此时 B 已经是一级缓存中的成品了,A 可以顺利完成属性填充和初始化。
  11. A 放入一级缓存:A 完成创建,放入一级缓存。

关键认知

  • 三级缓存解决的是"字段注入"和"Setter 注入"的循环依赖。构造方法注入的循环依赖无法被三级缓存解决------因为构造方法执行时对象还没创建出来,根本来不及放入缓存。
  • 只有单例 Bean 的循环依赖能被解决。原型(Prototype)Bean 的循环依赖无法解决,因为原型 Bean 每次都是新创建,没有缓存可以提前暴露。
  • 三级缓存的核心设计思想是"提前暴露" ------在对象尚未完全初始化时,就将其引用暴露出去,让依赖它的其他 Bean 可以先拿到一个"半成品"来完成自己的创建,等所有依赖都就位后再回过头来把"半成品"变成"成品"。

代码示例

示例一:"造车"案例的 IoC 改造

传统写法(紧耦合)

java 复制代码
// 传统写法:每一层都主动创建依赖
class Tire { }

class Bottom {
    private Tire tire = new Tire();  // 紧耦合
}

class Framework {
    private Bottom bottom = new Bottom();  // 紧耦合
}

class Car {
    private Framework framework = new Framework();  // 紧耦合
    
    public void run() {
        System.out.println("Car is running");
    }
}

// 使用
public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.run();
    }
}

IoC 写法(松耦合)

java 复制代码
// IoC 写法:只声明依赖,由外部注入
class Tire { }

class Bottom {
    private final Tire tire;
    public Bottom(Tire tire) {   // 构造方法注入
        this.tire = tire;
    }
}

class Framework {
    private final Bottom bottom;
    public Framework(Bottom bottom) {
        this.bottom = bottom;
    }
}

class Car {
    private final Framework framework;
    public Car(Framework framework) {
        this.framework = framework;
    }
    
    public void run() {
        System.out.println("Car is running");
    }
}

// 容器负责组装(模拟 Spring 的 IoC 容器)
public class IoCMain {
    public static void main(String[] args) {
        // 容器负责创建所有对象并组装依赖关系
        Tire tire = new Tire();
        Bottom bottom = new Bottom(tire);
        Framework framework = new Framework(bottom);
        Car car = new Car(framework);
        
        car.run();
    }
}

关键观察 :在 IoC 写法中,Car 不需要知道 Framework 是怎么创建的,Framework 不需要知道 Bottom 是怎么创建的------创建依赖的职责从对象本身转移到了容器

示例二:循环依赖的演示

使用字段注入(循环依赖可以被 Spring 解决)

java 复制代码
package com.example.demo.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {
    @Autowired
    private B b;
    
    public void say() {
        System.out.println("A says: " + (b != null ? "B is injected" : "B is null"));
    }
}

@Component
public class B {
    @Autowired
    private A a;
    
    public void say() {
        System.out.println("B says: " + (a != null ? "A is injected" : "A is null"));
    }
}
java 复制代码
package com.example.demo;

import com.example.demo.component.A;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = 
            SpringApplication.run(DemoApplication.class, args);
        
        A a = context.getBean(A.class);
        a.say();  // 输出: A says: B is injected
        
        context.close();
    }
}

使用构造方法注入(循环依赖会导致启动失败)

java 复制代码
@Component
public class A {
    private final B b;
    
    public A(B b) {   // 构造方法注入
        this.b = b;
    }
}

@Component
public class B {
    private final A a;
    
    public B(A a) {   // 构造方法注入
        this.a = a;
    }
}

启动时会报错:

复制代码
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
|  a (field injection)
↑     ↓
|  b (field injection)
└─────┘

为什么构造方法注入的循环依赖无法解决? 因为构造方法执行时对象还没创建出来,Spring 无法将其提前暴露到缓存中。所以官方推荐构造方法注入 的另一个侧面是:它能帮助你尽早发现循环依赖问题,而不是让 Spring 偷偷帮你解决掉------有些循环依赖本身就是设计问题,用字段注入掩盖了它。

新手错误 vs 正确姿势

错误表象 根本原因 正确姿势
@Autowired 注入的字段为 null,调用时抛出 NullPointerException Bean 未被 Spring 管理,或未被 @ComponentScan 扫描到 检查类是否添加了 @Component(或衍生注解),检查 @ComponentScan 的包路径是否覆盖了该类
同一个类型有多个 Bean,@Autowired 注入时报 NoUniqueBeanDefinitionException 容器中有多个同类型的候选 Bean,Spring 不知道选哪个 使用 @Primary 标记首选 Bean,或使用 @Qualifier 指定具体的 Bean 名称
构造方法注入时报循环依赖错误,但改成字段注入就正常了 构造方法注入的循环依赖无法被三级缓存解决 重新审视设计,是否存在不必要的循环依赖;如果确实需要,考虑用 @Lazy 延迟加载其中一个依赖
@Configuration 类中调用 @Bean 方法时,每次调用都返回新的实例 未理解 @Configuration 类中的 @Bean 方法会被 CGLIB 代理,Spring 会拦截调用并返回容器中的单例 正常使用即可------Spring 保证同一个 @Bean 方法在同一个容器中返回的是同一个实例(单例)

疑难深度追问

Q1:为什么说"Spring 是一个 IoC 容器"而不是"Spring 是一个框架"?

因为 IoC 是 Spring 最核心的底层机制 ,其他所有功能(AOP、事务管理、MVC、数据访问)都建立在 IoC 容器之上。BeanFactory 是 Spring IoC 容器的实际代表,负责包含和管理 Bean。没有 IoC 容器,AOP 无法织入、事务无法管理、Web 控制器无法被发现------整个 Spring 生态就失去了根基。

Q2:三级缓存为什么能解决循环依赖?

核心在于 "提前暴露" ------在对象尚未完全初始化时,就将其引用暴露出去。三级缓存中的 ObjectFactory 是一个函数式接口,只有在调用 getObject() 时才会真正创建对象。当 A 依赖 B、B 依赖 A 时,A 在实例化后立即将 ObjectFactory 放入三级缓存,B 在创建过程中可以通过这个工厂拿到 A 的早期引用(半成品),从而完成自己的创建。等 B 完成后,A 再继续完成自己的初始化。整个过程的关键是:允许对象在"未完成"状态下被其他对象引用,等所有依赖就位后再完成初始化

Q3:如果多个 @ControllerAdvice 同时存在,它们的执行顺序如何确定?

通过 @Order 注解或实现 Ordered 接口。数字越小,优先级越高,执行越靠前。如果没有指定顺序,则按 Spring 的默认顺序(通常是加载顺序)执行。

思考与延伸

  1. 动手验证:用字段注入和构造方法注入分别实现一个简单的循环依赖(A 依赖 B,B 依赖 A),观察 Spring 启动时的行为差异。你会直观地看到为什么官方推荐构造方法注入。

  2. 思考题 :如果把 @Autowired 用在 static 静态字段上,能注入成功吗?为什么?

  3. 延伸阅读 :Spring 官方文档的 "The IoC Container" 章节对 BeanFactoryApplicationContext 有详细的说明。另外,DefaultSingletonBeanRegistry 的源码是理解三级缓存的最佳入口。

参考与延伸阅读

  • Spring Framework. The IoC Container. Spring Framework Documentation, 6.0.x
  • Spring Framework. The BeanFactory API. Spring Framework Documentation
  • 阿里云开发者社区. 详细解析Spring如何解决循环依赖问题. 2026-03-25
  • 阿里云开发者社区. Spring面试必问:手写Spring IoC 循环依赖底层源码剖析. 2024-12-04
  • 腾讯云. Spring三种注入方式对比. 2025-12-22
  • Spring Framework. Dependency Injection. Spring Framework Documentation