如果你是从第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 写法
假设你要造一辆汽车。汽车依赖底盘,底盘依赖车轮。在传统编程中,你会这样写:
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 容器有两个核心接口:BeanFactory 和 ApplicationContext。
BeanFactory 是 Spring IoC 容器的根接口 ,定义了容器最核心的能力:管理 Bean 的实例化、配置和依赖装配。它的最主要方法就是 getBean(String beanName)。
ApplicationContext 是 BeanFactory 的子接口 ,在 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 的处理过程有完全控制时。对日常开发来说,记住一点就够了:你用的一直是 ApplicationContext,BeanFactory 是它的底层基础。
三、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 为例):
- 创建 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 改造
传统写法(紧耦合) :
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 的默认顺序(通常是加载顺序)执行。
思考与延伸
-
动手验证:用字段注入和构造方法注入分别实现一个简单的循环依赖(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