第一篇:Spring IoC容器——控制反转的本质与Bean的生命周期

前言

在面试中,Spring 是必问的框架。而所有关于 Spring 的问题,最终都会追溯到它的核心:IoC 容器

"Spring 的 IoC 是什么?控制反转到底反转了什么?"

"Bean 的生命周期是怎样的?"

"循环依赖是怎么解决的?三级缓存分别存了什么?"

这些问题考察的不是"会不会用 Spring",而是"理不理解 Spring 的设计思想"。本文从控制反转的本质出发,拆解 IoC 容器的核心机制、Bean 的完整生命周期,以及 Spring 解决循环依赖的经典设计。

本文核心问题:

  1. IoC 是什么?"控制反转"到底反转了什么?
  2. DI 和 IoC 是什么关系?它们是同一个概念吗?
  3. BeanFactory 和 ApplicationContext 有什么区别?
  4. Bean 的生命周期是怎样的?从实例化到销毁经历了哪些关键阶段?
  5. 依赖注入有几种方式?构造器注入和字段注入各有什么优劣?
  6. 循环依赖是怎么解决的?三级缓存分别存了什么?
  7. 为什么构造器注入无法解决循环依赖?

读完本文,你将对 Spring IoC 容器拥有从设计思想到源码实现的完整理解。


一、什么是 IoC------控制反转的本质

疑问:IoC 是"控制反转",到底反转了什么?为什么需要它?

回答:IoC 反转的是"对象创建的控制权"。传统方式是你自己 new 对象,IoC 是容器帮你创建并注入依赖。

1.1 没有 IoC 时

java 复制代码
public class OrderService {
    // 自己控制依赖的创建
    private OrderMapper orderMapper = new OrderMapperImpl();
    private UserService userService = new UserServiceImpl();
    
    public void createOrder(Order order) {
        userService.checkUser(order.getUserId());
        orderMapper.insert(order);
    }
}

问题

  1. OrderService 紧耦合于具体的实现类(OrderMapperImpl),换一个实现就要改代码
  2. 每个类都要自己管理依赖,依赖多了代码混乱
  3. 无法方便地替换依赖------测试时想用 Mock 对象替代真实实现,需要改动业务代码

1.2 有 IoC 时

java 复制代码
public class OrderService {
    // 只需要声明依赖,不需要自己创建
    private OrderMapper orderMapper;
    private UserService userService;
    
    // 构造器注入:容器帮你注入
    public OrderService(OrderMapper orderMapper, UserService userService) {
        this.orderMapper = orderMapper;
        this.userService = userService;
    }
    
    public void createOrder(Order order) {
        userService.checkUser(order.getUserId());
        orderMapper.insert(order);
    }
}

控制反转的核心:OrderService 不再控制"用什么 OrderMapper、怎么创建它",这个控制权交给了容器。OrderService 只需要声明自己需要什么,容器负责提供。对象创建和依赖注入的控制权从业务代码转移到了框架------这就是"控制反转"。


二、IoC 和 DI------同一个硬币的两面

疑问:IoC 和 DI(依赖注入)是同一个概念吗?

回答:IoC 是目的,DI 是实现。 IoC 描述的是"控制权从应用转移到框架"这个设计思想。DI 是 Spring 实现 IoC 的具体手段------容器在创建 Bean 时,自动把它的依赖"注入"进去。

两者的关系就像"自动挡汽车"和"自动变速箱"------自动挡是理念(IoC),自动变速箱是实现(DI)。

依赖注入的三种方式

方式 示例 优点 缺点
构造器注入 通过构造方法传入 依赖不可变,保证创建时完整性 参数多时构造器冗长
Setter 注入 通过 set 方法设置 可选依赖更灵活 依赖可变,可能创建未完全初始化的对象
字段注入 @Autowired 直接注入字段 写法简单 不可测试(需反射注入),依赖不明确,Spring 侵入性强

生产环境推荐构造器注入

  • 依赖不变量:一旦创建,依赖不可变更
  • 强制初始化:没有构造器传参,无法创建对象------编译期就发现依赖缺失
  • 易于测试:写单元测试时,直接通过构造器传入 Mock 对象即可,不需要启动 Spring 容器

三、BeanFactory vs ApplicationContext

疑问:BeanFactory 和 ApplicationContext 有什么区别?实际开发中用哪个?

回答:BeanFactory 是 Spring 最底层的 IoC 容器接口,提供最基本的 Bean 管理能力。ApplicationContext 继承了 BeanFactory,并扩展了更多高级特性。

维度 BeanFactory ApplicationContext
Bean 实例化时机 懒加载,第一次使用时才创建 预初始化,容器启动时创建所有单例 Bean
AOP 支持 需要手动处理 自动支持
国际化(MessageSource) 不支持 支持
事件发布(ApplicationEvent) 不支持 支持
可用的扩展点 较少(BeanPostProcessor 需手动注册) 丰富(自动注册 BeanPostProcessor 等)

实际开发统一使用 ApplicationContext。 BeanFactory 只在极端资源受限的场景(如 Applet、移动设备)中有应用。SpringBoot 默认使用的 AnnotationConfigApplicationContext 就是 ApplicationContext 的一种实现。


四、Bean 的生命周期------从实例化到销毁

疑问:一个 Bean 从创建到销毁,经历了哪些阶段?

回答:Bean 的生命周期分为四个核心阶段------实例化、属性赋值、初始化、销毁。每个阶段前后都有扩展点可以介入。

复制代码
Spring Bean 生命周期:

1. 实例化
   └── 调用构造器创建对象
   └── 扩展点:BeanPostProcessor.postProcessBeforeInstantiation → 可返回代理对象替代原 Bean

2. 属性赋值
   └── 注入依赖(@Autowired、@Value 等)
   └── 扩展点:Aware 系列接口 → 注入容器对象
        ├── BeanNameAware → 获取自己在容器中的名字
        ├── BeanFactoryAware → 获取 BeanFactory
        └── ApplicationContextAware → 获取 ApplicationContext
   └── 扩展点:BeanPostProcessor.postProcessBeforeInitialization

3. 初始化
   └── @PostConstruct 方法执行
   └── InitializingBean.afterPropertiesSet() 执行
   └── 自定义 init-method 执行
   └── 扩展点:BeanPostProcessor.postProcessAfterInitialization → AOP 在这里完成代理

4. 使用阶段
   └── Bean 对外提供服务

5. 销毁
   └── @PreDestroy 方法执行
   └── DisposableBean.destroy() 执行
   └── 自定义 destroy-method 执行

面试时最常被追问的两个时间点

  1. AOP 在哪里完成? BeanPostProcessor.postProcessAfterInitialization。Spring 在这里判断 Bean 是否需要被代理,如果需要,返回 JDK 或 CGLIB 代理对象
  2. @PostConstruct 和 InitializingBean 的执行顺序? @PostConstruct 先于 afterPropertiesSet,后者先于 init-method

五、Bean 的作用域------单例 vs 原型

疑问:Spring 的 Bean 默认是单例的,什么情况该用原型?

回答:默认单例意味着整个容器中只有一个实例------所有引用同一个 Bean 的地方,拿到的都是同一个对象。这是用最简单的模型解决绝大多数无状态场景的依赖管理。

作用域 含义 适用场景
singleton 容器中只有一个实例 无状态 Service、Dao、工具类(默认)
prototype 每次获取都创建新实例 有状态的 Bean,如购物车、用户会话数据
request 每个 HTTP 请求一个实例 Web 应用中同一请求的多个 Bean 共享数据
session 每个 HTTP Session 一个实例 用户会话级别的数据共享

prototype 的一个陷阱 :如果你在单例 Bean 中注入了一个原型 Bean,原型 Bean 只在创建单例 Bean 时被创建一次------它不会再被重新创建。要解决这个问题,需要注入 ObjectFactory 或使用 @Lookup 注解,每次获取时由容器动态返回新实例。


六、循环依赖------Spring 如何解决?

疑问:什么是循环依赖?Spring 是怎么解决的?

回答:循环依赖指 A 依赖 B,B 依赖 A,形成闭环。Spring 通过三级缓存机制解决了单例 Bean 的循环依赖问题,但构造器注入的循环依赖无法解决。

6.1 三级缓存

java 复制代码
// DefaultSingletonBeanRegistry 中的三级缓存

// 一级缓存:存放完全创建好的单例 Bean
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存:存放半成品 Bean(已实例化但未完成属性赋值和初始化)
Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三级缓存:存放 Bean 的工厂对象(用于生成 Bean 的早期引用)
Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

6.2 解决过程

复制代码
场景:A 依赖 B,B 依赖 A(都是单例、Setter注入)

1. 创建 A → 实例化 A(调用构造器)→ 得到 A 的半成品
2. 将 A 的工厂对象放入三级缓存(ObjectFactory)
3. 给 A 注入 B → 发现容器中没有 B → 开始创建 B
4. 创建 B → 实例化 B → 得到 B 的半成品
5. 将 B 的工厂对象放入三级缓存
6. 给 B 注入 A → 从三级缓存中找到 A 的工厂 → 获取 A 的半成品
7. 把 A 的半成品从三级缓存移到二级缓存
8. B 拿到 A 的半成品引用 → B 继续完成属性赋值和初始化 → B 创建完成
9. B 放入一级缓存
10. 回到 A 的注入过程 → 从一级缓存获取 B → A 继续完成初始化
11. A 从二级缓存移到一级缓存
12. 循环依赖解决

6.3 为什么构造器注入无法解决循环依赖?

三级缓存解决循环依赖的关键是"先实例化,后注入"------实例化和属性赋值分两步走。构造器注入在实例化这步就需要传入依赖对象,而此时依赖对象可能还没有创建,形成"先有鸡还是先有蛋"的死锁。Spring 无法打断这个循环,直接抛出 BeanCurrentlyInCreationException

6.4 为什么三级缓存不是两级?

二级缓存存半成品,三级缓存存工厂对象。这两者的区别在于:半成品是已经实例化但未完成初始化的原始对象;工厂对象可以在返回之前对半成品做一层处理------AOP 代理就在这里完成。

如果一个 Bean 需要被 AOP 代理(如 @Transactional 注解需要生成代理对象),在放入二级缓存之前,三级缓存的 ObjectFactory 会先判断并生成代理对象,然后代理对象才进入二级缓存。如果直接用两级缓存,代理逻辑无处安放------B 拿到的 A 引用是原始对象而非代理对象,事务切面不会生效。三级缓存的工厂机制为 AOP 代理保留了提前介入的窗口。


七、面试中这样回答

面试官:"Spring 的 IoC 是什么?"

回答框架

"IoC 是控制反转,反转的是对象创建和依赖管理的控制权。传统开发中,对象自己通过 new 创建依赖,控制权在对象手里。IoC 容器统一管理所有对象的创建和注入,对象只需要声明自己需要什么,控制权从对象转移到容器。Spring 通过 DI(依赖注入)实现 IoC------容器在创建 Bean 时自动注入它依赖的其他 Bean。"

面试官:"循环依赖怎么解决的?"

回答框架

"Spring 用三级缓存解决单例 Bean 的循环依赖。一级缓存放完全创建好的 Bean,二级缓存放半成品,三级缓存放工厂对象。实例化和属性赋值分两步走------先实例化拿到半成品放进缓存,再为半成品注入属性。当 B 需要注入 A 时,即使 A 还没创建完,也能从缓存中拿到半成品先引用。构造器注入的循环依赖无法解决------构造器要求在实例化阶段就传入依赖,而此时依赖还没有被创建出来。"


总结

  • IoC 反转的是对象创建的控制权------从"自己 new"变成"容器帮你创建并注入"。DI 是 IoC 的具体实现手段
  • BeanFactory 是底层基础设施,ApplicationContext 是在其之上扩展了 AOP、事件、国际化等高级特性
  • Bean 的生命周期分为实例化→属性赋值→初始化→使用→销毁五个阶段 ,每个阶段都有扩展点可以介入。AOP 在 postProcessAfterInitialization 完成代理
  • 三级缓存解决循环依赖:一级完全体→二级半成品→三级工厂对象。三级缓存的工厂用来在返回半成品之前完成 AOP 代理
  • 构造器注入的循环依赖无法解决------因为构造器在实例化阶段就需要依赖,而此时半成品还没有诞生
  • 构造器注入推荐用于生产:依赖不可变、编译期检查、易于单元测试。字段注入虽然写法简洁,但依赖不明确且强侵入 Spring 框架

下一篇预告:Spring 原理(二)------Spring AOP:动态代理与切面编程的底层原理。拆解 JDK 动态代理和 CGLIB 的区别、AOP 的切面切点通知连接点到底是什么、以及项目中的权限注解和事务管理是如何通过 AOP 实现的。

相关推荐
小新同学^O^2 小时前
简单学习 --> Spring统一处理
java·学习·spring·统一功能处理
huohuopro2 小时前
Spring MVC 的核心知识点梳理
spring·mvc·状态模式
@SmartSi2 小时前
AgentScope Java 入门系列:Spring AI Alibaba 与 AgentScope 的定位与区别
java·spring·agentscope
Jul1en_2 小时前
【SpringCloud】OpenFeign 与 Gateway 讲解与部署
spring·spring cloud·gateway
她说可以呀2 小时前
JWT令牌检验用户是否登录
java·spring boot·spring·java-ee·maven
庞轩px2 小时前
第三篇:SpringMVC——一个HTTP请求在Spring中经历了什么?
网络协议·spring·http·springmvc·handlermapping·前端控制器
空中海11 小时前
02 ArkTS 语言与工程规范
java·前端·spring
亚历克斯神11 小时前
Java 25 模式匹配增强:让代码更简洁优雅
java·spring·微服务
云烟成雨TD13 小时前
Spring AI Alibaba 1.x 系列【49】状态图运行时引擎:CompiledGraph 源码解析
java·人工智能·spring