2026年4月8日 发布于 「后端技术栈」
写在前面

Spring 几乎是 Java 后端开发的“标配”框架,而IoC(Inversion of Control,控制反转)和 DI(Dependency Injection,依赖注入) 是 Spring 框架最核心的两大支柱。但很多人在实际开发中,只会用 @Autowired,对“为什么能用”“底层怎么做到的”却一知半解。面试时被问到“IoC 和 DI 有什么区别”“底层原理是什么”,常常卡壳。
本文讲什么? 本文将从传统开发痛点出发,讲清 IoC 与 DI 的核心概念与关系,配合代码示例直观对比新旧实现方式,并点明底层依赖的关键技术——反射,最后附上高频面试题,帮你系统建立起这条知识链路。

🔔 系列预告:本文为 Spring IoC/DI 原理系列的第一篇,后续将继续深入容器生命周期、循环依赖、Bean 作用域等进阶内容,欢迎持续关注。
一、痛点切入:为什么需要 IoC?
传统开发模式是怎样的? 来看一段代码:
// 传统模式:直接在代码中创建依赖对象 public class UserServiceImpl implements UserService { // 主动 new 依赖对象,控制权在开发者手中 private UserDao userDao = new UserDaoImpl(); @Override public void queryUser() { userDao.queryUser(); } } public class Test { public static void main(String[] args) { UserService userService = new UserServiceImpl(); userService.queryUser(); } }
这段代码看似简单,但弊端非常明显:
耦合度高:
UserServiceImpl与UserDaoImpl强绑定。如果要把UserDaoImpl替换为另一个实现(比如从 MySQL 切换到 Oracle),必须修改UserServiceImpl的代码。可维护性差:随着业务复杂度的提升,手动管理对象之间的依赖关系会让代码臃肿不堪。
可测试性差:想要对
UserService做单元测试,却绕不开它所依赖的真实UserDaoImpl实例,很难做 mock 替换。
IoC 的出现,正是为了解决这些问题。它的核心思想是:将对象的创建权、依赖的装配权从业务代码转移到容器,开发者只声明“我需要什么”,由容器自动“送上门”-40。
二、核心概念讲解:IoC(控制反转)
标准定义
IoC(Inversion of Control,控制反转) 是一种设计思想。它将原本由程序代码主动控制的对象创建、依赖管理、生命周期管理等权力,反转给外部的容器(即 Spring IoC 容器)来负责。
拆解理解
所谓“控制反转”,这里的 “控制”指的是对象的创建和依赖关系的控制权,“反转”是指这种控制权从应用程序代码中“反”转交给了 Spring 容器-4。
生活化类比
传统开发模式就像自己在家做饭:你要主动去超市买菜(创建依赖),洗菜、切菜、炒菜(使用依赖),整个过程完全由你控制。
IoC 模式就像去餐厅吃饭:你(应用程序代码)只需要告诉服务员“我要一份番茄炒蛋”(声明需要什么),厨师(IoC 容器)会负责完成采购、备菜、做菜等一系列操作,最后把成品直接端上桌。你完全不需要关心“菜从哪里来”“依赖怎么配”——控制权从你手中转移到了厨师手中-41。
作用与价值
IoC 的本质是解耦。它将对象之间的依赖关系从代码中剥离出来,通过配置或注解管理,让业务逻辑更纯粹-40。
三、关联概念讲解:DI(依赖注入)
标准定义
DI(Dependency Injection,依赖注入) 是 IoC 的一种具体实现方式。它指的是:容器在创建 Bean 时,自动将该 Bean 所依赖的其他 Bean,通过构造器、Setter 方法或字段等方式“注入”到目标 Bean 中-4。
与 IoC 的关系
这是面试中最高频的考点。简单来说:IoC 是“思想”,DI 是“手段”-。
| 维度 | IoC(控制反转) | DI(依赖注入) |
|---|---|---|
| 本质 | 设计思想、原则 | 具体实现技术 |
| 回答的问题 | “谁来控制?” | “如何传递?” |
| 范畴 | 宽泛,涵盖程序流程控制 | 具体,专注于依赖关系管理 |
一句话概括:IoC 告诉你要“反转控制权”,DI 告诉你“通过注入的方式来实现” ——没有 DI,IoC 就缺少可落地的技术支撑;没有 IoC,DI 就失去了目标语境-。
DI 的三种注入形式
Spring 支持三种常见的注入方式-2:
1. 构造器注入(Constructor Injection)
@Service public class UserService { private final UserRepository userRepository; // Spring 容器在创建 UserService 时,自动将 UserRepository 注入 @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } }
特点:依赖不可变(可声明为 final),确保依赖不为空,推荐使用。
2. Setter 方法注入(Setter Injection)
@Service public class UserService { private UserRepository userRepository; @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } }
特点:依赖可选,可后续修改。
3. 字段注入(Field Injection)
@Service public class UserService { @Autowired private UserRepository userRepository; }
特点:代码最简洁,但不推荐在生产环境中使用,因为它违背单一职责原则、不利于单元测试,且可能导致 NPE 问题-34。
四、新旧对比 + 代码示例
下面用同一场景对比传统模式与 IoC/DI 模式的区别,让你直观感受变化。
场景:UserService 依赖 UserDao
传统模式(高耦合,无 IoC)
// 依赖对象 public class UserDaoImpl implements UserDao { @Override public void queryUser() { System.out.println("查询用户信息"); } } // 目标对象:手动创建依赖 public class UserServiceImpl implements UserService { // 主动 new 依赖对象,控制权在开发者手中 private UserDao userDao = new UserDaoImpl(); @Override public void queryUser() { userDao.queryUser(); } } // 测试类:手动创建所有对象 public class Test { public static void main(String[] args) { UserService userService = new UserServiceImpl(); userService.queryUser(); } }
IoC/DI 模式(低耦合,由容器接管)
// 依赖对象:无需手动 new,交给 Spring 管理 @Repository public class UserDaoImpl implements UserDao { @Override public void queryUser() { System.out.println("查询用户信息"); } } // 目标对象:仅声明依赖,由容器注入 @Service public class UserServiceImpl implements UserService { // 仅声明依赖,不主动创建 private UserDao userDao; @Autowired // 容器自动注入 public void setUserDao(UserDao userDao) { this.userDao = userDao; } @Override public void queryUser() { userDao.queryUser(); } } // 测试类:从容器中获取对象,无需手动管理依赖 public class Test { public static void main(String[] args) { // 容器初始化,自动创建 Bean、装配依赖 ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); // 直接获取对象,依赖已自动注入 UserService userService = context.getBean(UserService.class); userService.queryUser(); } }
核心变化:控制权从开发者转移到 Spring 容器——对象的创建、依赖的装配、生命周期的管理,全由容器负责-40。
五、底层原理 / 技术支撑:反射是背后的“功臣”
看到这里你可能会好奇:Spring 容器到底是怎么做到“动态创建对象”“自动注入依赖”的?
答案的核心是:Java 反射机制(Reflection)。
反射是 Java 在运行时动态获取类的结构信息(属性、方法、构造器)并操作这些成员的能力——即使在编译时完全不知道要创建哪个类的实例,也能在运行时通过类名动态创建对象-11。
Spring IoC 容器在启动时做了这样几件事:
扫描与解析:扫描带
@Component、@Service等注解的类,或解析 XML/Java 配置,将信息封装为BeanDefinition(Bean 的“说明书”)-4。注册:将
BeanDefinition存入BeanDefinitionRegistry注册表。实例化:容器遍历注册表中的
BeanDefinition,利用反射调用构造器创建对象实例——这是反射最直接的体现。依赖注入:再次通过反射,扫描目标 Bean 中带有
@Autowired等注解的字段或方法,动态调用 setter 方法或直接为字段赋值,完成依赖注入-4。
简而言之:Spring 通过反射机制,绕过了 new 关键字的编译期限制,实现了“运行时按需创建对象和注入依赖”的能力。很多主流框架(如 Hibernate ORM 映射、Jackson JSON 序列化)也都依赖反射实现核心功能-11。
六、高频面试题与参考答案
1. 请解释什么是 IoC?什么是 DI?两者的关系是什么?
参考答案:
IoC(控制反转) 是一种设计思想。它将对象的创建和依赖管理的控制权从应用程序代码反转给外部容器(Spring IoC 容器)。
DI(依赖注入) 是实现 IoC 思想的一种具体技术手段。容器在创建 Bean 时,自动将 Bean 所需的依赖对象通过构造器、Setter 方法或字段等方式注入进去。
两者的关系是“思想与实现”的关系:IoC 是“指导思想”,DI 是“落地手段”。没有 DI,IoC 无法落地;没有 IoC,DI 就失去了目标语境。
得分点:反转 / 解耦 / 注入 / 思想 vs 实现
2. Spring 实现 IoC 的底层依赖什么技术?
参考答案:
Spring IoC 底层主要依赖 Java 反射机制。容器在启动时通过扫描和解析配置,将 Bean 的定义信息封装为 BeanDefinition;在实例化阶段,利用反射动态调用构造器创建对象;在属性填充阶段,再次通过反射为带有 @Autowired 等注解的字段或方法完成依赖注入。还结合了工厂模式、单例模式等设计模式。
得分点:反射机制 / BeanDefinition / 工厂模式
3. @Autowired 和 @Resource 有什么区别?
参考答案:
@Autowired是 Spring 提供的注解,默认按类型(byType) 匹配。当存在多个同类型 Bean 时,需配合@Qualifier指定名称,否则会抛出NoUniqueBeanDefinitionException。@Resource是 JSR-250 标准注解,默认按名称(byName) 匹配,可通过name属性显式指定。若未指定名称且按名称找不到匹配的 Bean,会退回到按类型匹配。日常开发中推荐优先使用
@Autowired + @Qualifier,语义清晰,与 Spring 生态一致。
4. 构造器注入、Setter 注入、字段注入各有什么优缺点?推荐使用哪种?
参考答案:
| 注入方式 | 优点 | 缺点 |
|---|---|---|
| 构造器注入 | 依赖不可变(可声明为 final),确保依赖不为空,便于单元测试 | 参数较多时代码略臃肿 |
| Setter 注入 | 依赖可选,可后续修改 | 依赖可能为空,安全性较低 |
| 字段注入 | 代码最简洁 | 违背单一职责原则、不利于测试、可能产生 NPE |
推荐使用构造器注入。它能确保依赖不可为空,便于测试,同时符合对象创建完成后状态即就绪的设计原则。
七、结尾总结
回顾本文核心知识点:
IoC(控制反转) 是一种设计思想,将对象创建和依赖管理的控制权从代码转移到容器。
DI(依赖注入) 是实现 IoC 的具体手段,通过构造器、Setter、字段等方式注入依赖。
两者的关系:IoC 是思想,DI 是实现——IoC 回答“谁控制”,DI 回答“如何传”。
代码对比:传统模式需要手动
new对象,耦合度高;IoC/DI 模式由容器接管,低耦合、高内聚。底层原理:Spring 主要依赖 Java 反射机制实现对象的动态创建和依赖注入。
面试要点:能讲清 IoC 与 DI 的关系、说出底层依赖反射、分清三种注入方式的优劣。
📌 重点强调:IoC/DI 是 Spring 的基石,理解它不只是为了通过面试,更是写出高质量、低耦合代码的前提。不要只会用 @Autowired,要理解背后的“为什么”。
本文为系列文章首篇。下篇预告:Spring IoC/DI 原理(二):Bean 生命周期全解析 + 三级缓存如何破解循环依赖——从“容器怎么创建 Bean”到“A 依赖 B、B 依赖 A 怎么解决”,带你一探 Spring 源码级别的巧妙设计,敬请期待!