Spring 的 IoC/DI 原理:从“我会用”到“我懂它”
一句话速览:本文从传统代码的“耦合困境”出发,拆解 IoC(控制反转)的设计思想与 DI(依赖注入)的实现机制,通过极简代码示例带你搞懂 Spring 容器到底帮你做了什么,顺便拿下面试中的高频考点。

一、开篇:为什么这篇文章值得你读完?
Spring 是 Java 后端开发的“基石框架”,而 IoC(Inversion of Control,控制反转) 和 DI(Dependency Injection,依赖注入) 正是 Spring 的两大灵魂所在。无论你正在使用 Spring Boot 还是传统的 Spring Framework,底层支撑这一切的,始终是 IoC 容器-7。

但很多开发者的真实状况是:
会用 @Autowired,但被问到“Spring 到底是怎么把它塞进去的”就卡壳了;
听过 IoC 和 DI,但经常把这两个概念搞混;
背过面试题,但面试官稍微追问就露馅。
本文的目标就是带你走出“只会用、不懂原理”的尴尬,真正理解 IoC 和 DI 的本质。全文围绕一个核心主线展开:问题 → 概念 → 关系 → 示例 → 原理 → 考点。
二、痛点切入:没有 IoC 的日子,代码有多痛?
先来看一段没有 Spring 的“原始”代码:
// 依赖对象:数据访问层public class UserDaoImpl implements UserDao { public void queryUser() { System.out.println("查询用户信息"); }}// 目标对象:业务服务层 —— 手动创建依赖,控制权在开发者手中public class UserServiceImpl implements UserService { private UserDao userDao = new UserDaoImpl(); // ⚠️ 主动 new,强耦合 public void queryUser() { userDao.queryUser(); }}// 测试类:手动创建所有对象public class Test { public static void main(String[] args) { UserService userService = new UserServiceImpl(); userService.queryUser(); }}这段代码有什么问题?至少三条硬伤:
高耦合:
UserServiceImpl与UserDaoImpl强绑定。想换一种UserDao的实现(比如从 MySQL 切换到 Oracle),必须修改UserServiceImpl的代码-1;扩展性差:如果项目中有几十个 Service 对象,每个都需要手动维护依赖关系,代码臃肿不堪;
可测试性差:单元测试时无法轻松替换 Mock 对象,测试变得极其困难-1。
正是这些痛点,催生了 IoC 的设计思想——把“谁来创建对象”这个控制权,从开发者手中交给容器。
三、核心概念 A:IoC(控制反转)
标准定义
IoC(Inversion of Control,控制反转) 是一种设计原则,其核心思想是将对象的创建、依赖关系的管理以及生命周期的控制权,从应用程序代码本身转移到外部的容器或框架中-19。
拆解关键词
所谓“反转”,对比的是传统的“正转”:
| 对比维度 | 传统模式(正转) | IoC 模式(反转) |
|---|---|---|
| 谁创建对象 | 开发者手动 new | Spring 容器创建 |
| 谁管理依赖 | 开发者手动装配 | 容器自动注入 |
| 代码特点 | 高耦合、难维护 | 低耦合、易扩展 |
生活化类比
IoC 就像 “从亲自做饭到点外卖” 的转变:
传统模式:你自己买菜、洗切、烹饪、摆盘——你掌控了全过程,但也付出了全部精力;
IoC 模式:你打开 App 点单,告诉平台“我要一份红烧肉”,外卖平台(容器)自动完成采购、制作、配送——你把控制权交给了平台,只关注“吃什么”本身。
核心作用
IoC 解决的核心问题是 解耦——将对象与对象之间的依赖关系,从硬编码的代码中剥离出来,通过配置或注解来管理,让业务逻辑更纯粹-。统计显示,超过 80% 的 Spring 核心模块直接或间接依赖 IoC 容器提供的服务,包括 AOP 代理创建、事务管理拦截、MVC 控制器映射等-7。
四、关联概念 B:DI(依赖注入)
标准定义
DI(Dependency Injection,依赖注入) 是一种实现 IoC 的具体方式。容器在创建 Bean 时,自动将所依赖的其他 Bean 对象“注入”到目标 Bean 中,开发者无需手动获取-2。
DI 与 IoC 的关系
一句话概括:IoC 是“思想”,DI 是“手段”。
IoC 是设计原则,定义了“谁来掌控”的问题;
DI 是 Spring 实现 IoC 的具体技术方案,解决了“如何把依赖送过去”的问题-。
类比辅助理解
把 IoC 和 DI 的关系类比成 OOP 与 Java 类/接口 的关系:
OOP(面向对象编程) 是一种设计思想;
Java 的类和接口 是实现这种思想的技术工具-。
同样地:
IoC 是一种设计思想;
DI 是 Spring 实现这种思想的技术手段。
DI 的三种实现方式
Spring 中 DI 主要有三种形式,面试时经常被问到:
| 注入方式 | 写法示例 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| 构造器注入 | 通过有参构造注入 | 依赖不可变(final)、非空保证、便于测试 | 参数过多时代码冗长 | ⭐⭐⭐⭐⭐(大厂标配) |
| Setter 注入 | 通过 setXxx() 方法注入 | 灵活、支持可选依赖 | 可被外部修改为空,不安全 | ⭐⭐ |
| 字段注入 | 直接在字段上写 @Autowired | 代码简洁、开发效率高 | 无法注入 final、耦合度高、单元测试困难 | ⭐⭐⭐⭐(日常主力,但不推荐生产) |
构造器注入示例(最推荐) :
@Servicepublic class UserService { private final UserRepository repository; // final 确保不可变 // Spring 4.3+ 单一构造器可省略 @Autowired public UserService(UserRepository repository) { this.repository = repository; }}字段注入示例(最常用,但需谨慎) :
@Servicepublic class ProductService { @Autowired // 最简洁,但存在安全隐患 private CategoryService categoryService;}面试加分回答:生产环境优先使用构造器注入,可选依赖用 Setter 注入,尽量避免字段注入-51。
五、概念关系总结
| 对比维度 | IoC(控制反转) | DI(依赖注入) |
|---|---|---|
| 本质 | 设计思想 / 设计原则 | 具体实现方式 / 技术手段 |
| 解决的问题 | 谁来掌控对象的生命周期? | 如何把依赖对象传进去? |
| 在 Spring 中的角色 | 宏观的设计目标 | 实现该目标的具体机制 |
| 一句话记忆 | 思想:控制权交出去 | 手段:依赖送进来 |
一句话高度概括:IoC 告诉你“不用自己 new 对象”,DI 告诉 Spring “帮我把它塞进去”。
六、代码示例:让容器帮你干活
下面用注解配置的方式,展示 Spring IoC 容器的完整工作流程:
1. 定义组件(Dao + Service)
// 依赖对象:数据访问层 —— 无需手动 new@Repositorypublic class UserDaoImpl implements UserDao { @Override public void queryUser() { System.out.println("查询用户信息"); }}// 目标对象:业务服务层 —— 仅声明依赖,不主动创建@Servicepublic class UserServiceImpl implements UserService { // 只声明依赖,由容器注入 private UserDao userDao; // 提供注入入口(Setter 注入示例,也可改用构造器) @Autowired public void setUserDao(UserDao userDao) { this.userDao = userDao; } @Override public void queryUser() { userDao.queryUser(); }}2. 从容器中获取并使用
public class Test { public static void main(String[] args) { // 容器初始化,自动创建 Bean、装配依赖 ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); // 直接获取对象,依赖已自动注入 UserService userService = context.getBean(UserService.class); userService.queryUser(); }}3. 核心变化对比
| 对比维度 | 传统方式 | Spring IoC 方式 |
|---|---|---|
| 对象创建 | new UserDaoImpl() | 容器自动创建 |
| 依赖管理 | 手动 setXxx() | 容器自动注入 |
| 代码关注点 | “怎么得到依赖” | “需要什么依赖” |
| 扩展性 | 修改代码 | 调整配置/注解 |
核心变化:控制权从开发者转移到 Spring 容器——对象的创建、依赖的装配、生命周期的管理,全由容器负责。开发者只需声明“我需要什么依赖”,容器就会自动将依赖“送上门”-1。
七、底层原理:容器到底怎么做的?
⚠️ 本节提示:不深入源码细节,只做关键原理定位,为后续进阶内容预留空间。
核心一句话
Spring IoC 的底层 = 工厂模式 + Java 反射机制-19。
四步核心流程(以注解配置为例)
| 步骤 | 做什么 | 关键机制 |
|---|---|---|
| Step 1 | 容器启动,扫描注解类,封装成 BeanDefinition | 注解解析 |
| Step 2 | 将 BeanDefinition 注册到容器注册表(本质是 Map<String, BeanDefinition>) | 注册表管理 |
| Step 3 | 根据 BeanDefinition 创建 Bean 实例,完成依赖注入 | 反射机制 |
| Step 4 | 执行初始化回调,Bean 就绪可用 | 生命周期回调 |
BeanDefinition:Bean 的“说明书”
BeanDefinition 是 Spring 对 Bean 的高度抽象,它包含了 Bean 的所有信息:类全限定名、作用域(singleton/prototype)、延迟初始化标志、依赖关系、初始化/销毁方法等二十余种配置属性。这种元数据驱动的设计模式,使得 Spring 能够在运行时动态调整 Bean 的行为特征-7。
反射机制在其中的角色
反射允许程序在运行时获取类的结构(字段、方法、注解)并动态操作对象,这打破了 Java “编译期确定”的传统约束-。Spring 正是利用反射来:
实例化 Bean:根据
BeanDefinition中的类全限定名,通过Class.forName()加载类,再调用构造器创建实例;执行依赖注入:扫描字段或方法上的
@Autowired注解,通过反射设置字段值或调用方法;调用生命周期方法:反射调用
@PostConstruct等初始化方法。
一句话理解:反射让 Spring 在写代码时不知道你要创建什么对象,但运行后依然能把它“造出来”。
八、高频面试题与参考答案
面试题 1:IoC 和 DI 的区别是什么?
参考答案:
IoC(Inversion of Control,控制反转) 是一种设计思想,核心是将对象的创建权和依赖管理权从业务代码中“反转”给 Spring 容器管理-51。
DI(Dependency Injection,依赖注入) 是 IoC 的具体实现方式,Spring 通过 DI 在创建 Bean 时自动将依赖对象注入进来-2。
一句话总结:IoC 是“思想”,DI 是“手段”-。
踩分点:先分别定义,再说明关系,最后给出总结性一句话。
面试题 2:Spring 中有哪几种依赖注入方式?分别有什么优缺点?
参考答案:有三种主要方式-51:
| 方式 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 构造器注入 | 依赖不可变(final)、非空保证、便于测试、Spring 官方推荐 | 参数过多时代码冗长 | 所有必填依赖 |
| Setter 注入 | 灵活、支持可选依赖 | 可被外部改为 null、不安全 | 可选依赖、老项目维护 |
| 字段注入 | 代码简洁、开发效率高 | 无法注入 final、单元测试困难、耦合度高 | 日常开发(需谨慎) |
踩分点:答全三种方式 + 每个方式的优缺点 + 官方推荐(构造器注入)。
面试题 3:Spring IoC 的底层原理是什么?
参考答案:Spring IoC 的底层主要依赖 工厂模式 + Java 反射机制-19。核心流程是:
容器启动时扫描配置(XML/注解),将每个 Bean 的信息封装成
BeanDefinition对象-2;将
BeanDefinition注册到容器注册表(本质是一个Map<String, BeanDefinition>)-2;实例化阶段:容器根据
BeanDefinition中的全类名,通过反射调用构造器创建 Bean 实例;依赖注入阶段:容器扫描字段或方法上的
@Autowired注解,通过反射完成属性赋值-7;执行初始化回调,Bean 就绪可用。
踩分点:点出“工厂模式+反射”核心组合 + 简述 BeanDefinition 的关键作用 + 说明反射在实例化和注入两处的具体应用。
面试题 4:BeanFactory 和 ApplicationContext 有什么区别?
参考答案:
| 对比维度 | BeanFactory | ApplicationContext |
|---|---|---|
| 继承关系 | 顶层基础接口 | 继承自 BeanFactory,功能增强版 |
| Bean 加载策略 | 懒加载(调用 getBean() 时才创建) | 非懒加载(启动时创建所有单例 Bean) |
| 功能丰富度 | 仅有基础 DI 功能 | 额外支持国际化、事件发布、资源加载、AOP 等-7 |
| 日常使用 | 较少直接使用 | 开发首选 |
踩分点:说清继承关系 + 核心区别(加载策略 + 功能) + 明确推荐使用 ApplicationContext-2。
面试题 5:Spring 中的 Bean 默认是什么作用域?有哪几种作用域?
参考答案:Bean 的默认作用域是 singleton(单例),即在整个 Spring IoC 容器中,每个名称的 Bean 只有一个实例-37。Spring 支持 6 种作用域-:
singleton(默认):单例,适合无状态的 Service、DAO 层组件;
prototype:原型,每次获取都创建新实例,适合有状态对象;
request:每个 HTTP 请求一个实例(Web 环境);
session:每个 HTTP Session 一个实例(Web 环境);
application:每个 ServletContext 一个实例;
websocket:每个 WebSocket 一个实例。
踩分点:默认作用域(singleton)+ 两种常用作用域的对比 + 面试官追问时能说出 prototype 的场景适用性。
九、结尾总结
全文核心回顾
| 知识点 | 一句话总结 |
|---|---|
| IoC | 把对象的创建权、依赖管理权交给 Spring 容器,控制权被“反转” |
| DI | IoC 的具体实现方式,容器把依赖对象“注入”给目标 Bean |
| IoC vs DI | IoC 是“思想”,DI 是“手段” |
| 底层原理 | 工厂模式 + 反射机制 + BeanDefinition 元数据模型 |
| 注入方式 | 构造器注入(最推荐)、Setter 注入、字段注入 |
| Bean 作用域 | 默认 singleton,prototype 为多实例 |
重点与易错点
⚠️ 易混淆:IoC 是设计思想,DI 是实现手段,不要混为一谈。
⚠️ 易忽略:Spring Boot 虽然简化了配置,但底层依然是 IoC 容器在支撑,理解原理有助于排查问题-7。
⚠️ 易踩坑:字段注入虽然代码简洁,但生产环境应优先使用构造器注入,确保依赖不可变和可测试性。
下篇预告
下一篇我们将深入 Bean 的生命周期,详细拆解 Spring 容器中一个 Bean 从“出生”到“销毁”的全过程——实例化、属性填充、Aware 接口回调、初始化、使用、销毁,以及每个阶段中我们可以参与的扩展点。
📚 参考资料
[1] Spring核心概念:IoC与DI深度解析 (CSDN, 2026-04-04)-1
[2] 一文搞懂spring ioc底层原理 (博客园, 2026-03-11)-2
[3] 深入解析Spring IoC容器:从启动流程到BeanPostProcessor扩展点 (腾讯云, 2025-08-27)-7
[4] Spring依赖注入方式(构造器、Setter、注解) (CSDN, 2025-12-07)-28
[5] 面试官:说说Spring中IoC实现原理? (腾讯云, 2024-03-29)-19
[6] Spring Bean 一共有几种作用域? (面试鸭, 2026-04-02)-37
[7] Spring 全套高频面试题 (技术栈, 2026-01-15)-51
如果觉得本文有帮助,欢迎点赞、收藏、转发,让更多人看到~