发布于:2026年4月8日 北京时间 · 作者 | 编程面试通
一、开篇引入:为什么每一个 Java 开发者都要搞懂 IoC 和 DI?

在 Java 后端开发生态中,Spring 框架几乎无处不在。而支撑 Spring 这座大厦最核心的地基,就是 IoC(控制反转) 与 DI(依赖注入) 。
控制反转(Inversion of Control,IoC)是一种设计思想,它将对象创建和依赖管理的控制权从开发者代码转移到外部容器。依赖注入(Dependency Injection,DI)是实现 IoC 的具体方式——容器在创建 Bean 时,自动将依赖的 Bean 注入到目标 Bean 中(比如通过 @Autowired 注解)-1。

对于技术学习者、在校学生和面试备考者而言,理解 IoC 和 DI 不仅是为了写出低耦合的代码,更是理解 Spring 底层原理的关键。很多开发者“会用”Spring——写下 @Autowired、@Service,依赖就自动注入了,感觉很方便。但面试官一问“底层怎么实现的”,就卡住了。IoC 到底反转了什么?DI 又是如何把依赖“送进来”的?反射在其中扮演了什么角色?本文就是为你回答这些问题。
二、痛点切入:传统代码的“高耦合之痛”
在正式介绍 IoC 和 DI 之前,我们先看一个“造车”的例子——不借助 Spring,传统开发中代码是什么样子的:
// Tire.java - 最底层组件 public class Tire { int size; public Tire(Integer size) { this.size = size; System.out.println("轮胎初始化,尺寸:" + size); } } // Bottom.java - 底盘依赖轮胎 public class Bottom { private Tire tire; public Bottom(Integer size) { this.tire = new Tire(size); System.out.println("底盘初始化..."); } } // Framework.java - 车身依赖底盘 public class Framework { private Bottom bottom; public Framework(Integer size) { this.bottom = new Bottom(size); System.out.println("车身初始化..."); } } // Car.java - 汽车依赖车身 public class Car { private Framework framework; public Car(Integer size) { this.framework = new Framework(size); System.out.println("汽车初始化..."); } public void run() { System.out.println("汽车启动..."); } } // Main.java - 测试类 public class Main { public static void main(String[] args) { Car car = new Car(21); car.run(); } }
这段代码的问题非常明显:
高度耦合:从
Car到Framework再到Bottom、Tire,每一层都直接new出下一层的对象。当最底层的Tire尺寸发生变化时,整个调用链上的所有代码都需要修改-19。扩展性差:如果想将轮胎从 21 寸换成 18 寸,必须一层层修改构造函数参数,甚至修改
Tire的构造逻辑。可测试性差:单元测试时,无法轻易用 Mock 对象替换真实的依赖,因为代码已经硬编码了具体实现。
维护成本高:随着系统规模增长,手动管理所有依赖关系会让代码变得臃肿不堪。
这就是传统开发模式的困境——对象主动 new 依赖,导致代码“牵一发而动全身”。
三、核心概念讲解:IoC(控制反转)
标准定义
IoC 是 Inversion of Control 的缩写,中文译作“控制反转”。它是一种设计思想,而非具体技术实现-22。
拆解关键词
“控制” 指什么?——对象创建权、依赖管理权、生命周期管理权。
“反转” 怎么理解?——从开发者主动
new创建,变为被动接受容器提供。一句话总结:把对象创建、依赖管理的权力从开发者代码转移到 Spring 容器,核心是 “反转了对象的创建权” -1。
生活化类比
用“组织家庭聚餐”来类比最贴近日常-50:
传统模式(无 IoC):自己办聚餐。从列食材清单(确定依赖)、去超市采购(
new对象)、到洗菜备菜(关联依赖),全程亲力亲为。如果忘了买可乐,可乐鸡翅就没法做——所有事情自己掌控,也意味着所有错误自己承担。IoC 模式:找上门厨师(Spring 容器)。你只需要告诉厨师“周末中午 10 人聚餐,要 3 个热菜、2 个凉菜”(声明需求)。厨师会自己列清单、采购食材、备菜做菜,最后把做好的菜端上桌——你只管“吃”(专注业务逻辑),不用管食材怎么来、依赖怎么配。
IoC 的核心价值
IoC 的核心价值不是“少写几行 new 代码”,而是 “彻底解耦” ——将对象与对象之间的依赖关系从代码中剥离,通过配置或注解集中管理,让业务逻辑更加纯粹-2。
四、关联概念讲解:DI(依赖注入)
标准定义
DI 是 Dependency Injection 的缩写,中文译作“依赖注入”。它是 IoC 的具体实现方式之一-22。
DI 是如何工作的?
容器在创建 Bean 时,自动将依赖的 Bean 注入到目标 Bean 中(比如通过 @Autowired)。Spring 提供了三种主要的注入方式-5-24:
1. 构造器注入(推荐)
@Controller public class UserController { private final UserService userService; // Spring 容器会自动查找 UserService 类型的 Bean 并注入此处 public UserController(UserService userService) { this.userService = userService; } }
2. Setter 方法注入
@Component public class MyService { private MyDependency myDependency; @Autowired public void setMyDependency(MyDependency myDependency) { this.myDependency = myDependency; } }
3. 字段注入
@Component public class MyService { @Autowired private MyDependency myDependency; }
💡 注意:字段注入虽然最简洁,但不利于单元测试和不可变性。在 Spring Boot 官方推荐中,构造器注入是最佳实践。
生活化类比
继续用“聚餐”的例子:厨师把可乐倒进鸡翅锅、把鸡蛋打进番茄碗的动作,就是 DI(依赖注入) ——把对象需要的依赖“主动送过去”-50。
五、概念关系与区别总结
IoC 与 DI 的关系可以用一句话高度概括:
IoC 是“思想”,DI 是“手段”;IoC 是“设计”,DI 是“落地”。
| 维度 | IoC(控制反转) | DI(依赖注入) |
|---|---|---|
| 定位 | 设计思想 | 具体实现方式 |
| 作用 | 定义“谁来掌控对象” | 描述“如何把依赖送进来” |
| 层级 | 更高层次的抽象 | 底层落地技术 |
| 是否可替换 | 思想本身不变 | 实现方式可换(如依赖查找 DL) |
六、代码示例:从“主动 new”到“被动接收”
传统模式(高耦合)
// 依赖对象 public class UserDaoImpl implements UserDao { @Override public void queryUser() { System.out.println("查询用户信息"); } } // 目标对象——主动创建依赖 public class UserServiceImpl implements UserService { private UserDao userDao = new UserDaoImpl(); // ❌ 硬编码 @Override public void queryUser() { userDao.queryUser(); } }
IoC + DI 模式(低耦合)
// 依赖对象——由容器管理 @Repository public class UserDaoImpl implements UserDao { @Override public void queryUser() { System.out.println("查询用户信息"); } } // 目标对象——只声明需要什么,不主动创建 @Service public class UserServiceImpl implements UserService { private UserDao userDao; // 只声明,不 new @Autowired public void setUserDao(UserDao userDao) { // 容器主动注入 this.userDao = userDao; } @Override public void queryUser() { userDao.queryUser(); } } // 测试类——从容器获取,无需手动管理依赖 public class Test { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); UserService userService = context.getBean(UserService.class); userService.queryUser(); } }
对比总结
| 维度 | 传统模式 | IoC + DI 模式 |
|---|---|---|
| 对象创建 | 手动 new | 容器自动创建 |
| 依赖装配 | 硬编码绑定 | 自动注入 |
| 耦合度 | 高(改动一处,处处修改) | 低(只改配置/注解) |
| 可测试性 | 差(无法 Mock) | 好(易替换依赖) |
七、底层原理 / 技术支撑:反射机制
IoC 和 DI 能“活”起来,底层靠的是 Java 反射机制(Reflection)。Spring 底层通过 “工厂模式 + 反射” 实现 IoC 容器-。
反射机制在 IoC 中的作用
Spring 容器启动时,大致经历以下流程-1-5:
步骤 1:扫描配置元数据
开发者创建 ApplicationContext 实例时(如 new AnnotationConfigApplicationContext(AppConfig.class)),容器首先扫描带有 @Component、@Service、@Repository、@Controller 等注解的类。
步骤 2:封装 BeanDefinition
容器将扫描到的类封装成 BeanDefinition 对象——它包含了 Bean 的所有元信息:类全限定名、作用域(单例/原型)、依赖关系、初始化/销毁方法等。BeanDefinition 相当于 Bean 的“说明书”,告诉容器“这个 Bean 长什么样、依赖谁”-5。
步骤 3:注册到容器
BeanDefinition 被注册到 BeanDefinitionRegistry 中,本质是一个 Map<String, BeanDefinition>,key 是 Bean 名称,value 是 Bean 定义。
步骤 4:实例化与依赖注入(核心!)
容器根据 BeanDefinition 创建 Bean 实例——这里就是反射登场的地方:
// 简化版:反射调用构造器创建对象 Class<?> clazz = Class.forName(beanDefinition.getBeanClassName()); Object instance = clazz.getDeclaredConstructor().newInstance(); // 反射获取字段,进行依赖注入 Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(Autowired.class)) { Object dependency = getBean(field.getType()); // 递归获取依赖 field.setAccessible(true); // 突破 private 限制 field.set(instance, dependency); // 注入! } }
反射 vs new:对比理解
| 维度 | new 关键字 | 反射 |
|---|---|---|
| 时机 | 编译期确定 | 运行时动态 |
| 灵活性 | 低(类名硬编码) | 高(类名可配置) |
| 性能 | 较高 | 稍低(有优化空间) |
| 典型场景 | 普通业务代码 | 框架底层、容器 |
💡 关键结论:正是反射让 Spring 能够在运行时动态创建对象、调用方法、修改属性,从而支撑起 IoC 容器的核心能力。没有反射,IoC 就无从谈起。
八、高频面试题与参考答案
1. 谈谈你对 IoC 和 DI 的理解,它们之间是什么关系?
【参考答案】
IoC 是 Inversion of Control(控制反转),是一种设计思想——将对象创建和依赖管理的控制权从程序代码转移到外部容器。DI 是 Dependency Injection(依赖注入),是实现 IoC 的具体方式。两者是“思想与实现”的关系:IoC 定义“谁来掌控对象”,DI 描述“如何把依赖送进来”。Spring 通过 DI 这种具体技术来实现 IoC 的设计目标-47-。
【面试得分点】:思想 vs 实现、控制权转移、解耦
2. Spring IoC 容器底层是如何实现的?用到了哪些技术?
【参考答案】
Spring IoC 容器的底层实现主要包括四个步骤:
扫描配置元数据:读取注解(如
@Component)或 XML 配置。封装 BeanDefinition:将类信息封装成
BeanDefinition对象,包含类名、作用域、依赖关系等。注册到容器:将
BeanDefinition存入BeanDefinitionRegistry(本质是Map)。实例化与依赖注入:通过 Java 反射机制动态创建对象实例,并通过递归方式完成属性填充(依赖注入)。
核心技术是 “工厂模式 + 反射” --1。
【面试得分点】:BeanDefinition、BeanDefinitionRegistry、反射、工厂模式
3. DI(依赖注入)有哪几种方式?各自有什么优缺点?
【参考答案】
Spring 支持三种依赖注入方式:
| 注入方式 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 构造器注入 | public UserController(UserService userService) | 不可变、线程安全、推荐使用 | 注入较多时构造器参数冗长 |
| Setter 注入 | @Autowired public void setXxx(Xxx xxx) | 灵活、可选依赖 | 不能保证不可变性 |
| 字段注入 | @Autowired private UserService userService | 代码简洁 | 不利于单元测试,Spring 官方不推荐 |
推荐:优先使用构造器注入-47-24。
【面试得分点】:三种方式对比、构造器注入推荐、@Autowired
4. 请简述 Spring 中 Bean 的生命周期
【参考答案】
一个 Bean 从创建到销毁经历以下关键步骤:
实例化:容器通过反射调用构造器创建 Bean 实例。
属性赋值:容器注入 Bean 的依赖和其他配置属性。
BeanPostProcessor前置处理:调用postProcessBeforeInitialization。初始化:执行
@PostConstruct或init-method指定的方法。BeanPostProcessor后置处理:调用postProcessAfterInitialization(AOP 代理在此步创建)。使用:Bean 准备就绪,可供应用程序调用。
销毁:容器关闭时,执行
@PreDestroy或destroy-method指定的方法-47。
【面试得分点】:反射实例化、BeanPostProcessor、初始化/销毁钩子
九、结尾总结
回顾核心知识点
IoC(控制反转):一种设计思想,将对象创建和依赖管理的控制权从开发者转移到容器。
DI(依赖注入):IoC 的具体实现方式,容器自动将依赖“送”到对象中。
关系:IoC 是“思想”,DI 是“手段”。
底层实现:Spring 通过“工厂模式 + 反射”实现 IoC 容器,
BeanDefinition是 Bean 的元数据模型,反射负责运行时动态创建和注入。三种注入方式:构造器注入(推荐)、Setter 注入、字段注入。
重点与易错点
| 常见误区 | 正确理解 |
|---|---|
| “IoC 和 DI 是同一个东西” | ❌ IoC 是思想,DI 是实现方式 |
| “DI 只有一种注入方式” | ❌ 有三种:构造器、Setter、字段 |
| “反射性能太差,Spring 不用” | ❌ 反射是核心支撑技术,Spring 深度依赖它 |
| “字段注入最方便,所以最好” | ❌ 构造器注入才是官方推荐 |
下一篇预告
我们已经讲完了 IoC 和 DI 的核心概念、代码对比、底层原理(反射)以及面试考点。下一篇文章我们将继续深入:
《Spring IOC/DI 原理(二):Bean 生命周期详解与三级缓存解决循环依赖》
敬请期待!
📌 本文引用数据:
IoC 将对象创建权从开发者转移到 Spring 容器-1
底层靠“工厂模式 + 反射”实现-
DI 三种注入方式:构造器、Setter、字段-5
BeanDefinition 包含类名、作用域、依赖关系等元数据-5
超过 80% 的 Spring 核心模块直接或间接依赖 IoC 容器-5
扫一扫微信交流