欢迎光临
我们一直在努力

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

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

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

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

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

学习目标

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

正文

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

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

// 传统写法:上层主动创建下层依赖
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 的写法:把“创建对象”这件事交给容器,对象之间只声明“我需要什么”,而不关心“怎么得到”。

// 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 容器有两个核心接口:BeanFactory 和 ApplicationContext。

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

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

功能BeanFactoryApplicationContext
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 的处理过程有完全控制时。对日常开发来说,记住一点就够了:你用的一直是 ApplicationContext,BeanFactory 是它的底层基础。

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

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

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

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

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

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

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

方式二:@Bean 方法注解

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

@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
}

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

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

四、依赖注入的三种方式

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

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

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

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

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

@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)

@Service
public class UserService {
private UserMapper userMapper;

@Autowired
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
}

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

选型建议:

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

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

五、循环依赖与三级缓存

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

什么是循环依赖?

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

@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 类中:

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 为例):

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

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

    代码示例

    示例一:“造车”案例的 IoC 改造

    传统写法(紧耦合) :

    // 传统写法:每一层都主动创建依赖
    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 写法(松耦合) :

    // 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 解决) :

    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"));
    }
    }

    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();
    }
    }

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

    @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 的默认顺序(通常是加载顺序)执行。

    思考与延伸

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

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

  • 延伸阅读:Spring 官方文档的 “The IoC Container” 章节对 BeanFactory 和 ApplicationContext 有详细的说明。另外,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
    赞(0)
    未经允许不得转载:171主机测评 » 第10篇 · IoC容器深度解密:Spring的“心脏”如何跳动
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址