北京时间 2026年4月8日 | 目标读者:技术入门/进阶学习者、在校学生、面试备考者、相关技术栈开发工程师
〇、写在前面:你为什么需要读完这篇?

如果你用 Spring 开发过项目,大概率写过这样的代码:
@Servicepublic class UserService { @Autowired private UserDao userDao; }
然后就这样用了——会用,但讲不清为什么这么写、底层发生了什么、面试被问到时只能憋出一句“IoC就是控制反转,DI就是依赖注入”。
这篇文章的目标只有一个:帮你把IoC/DI这根“弹簧面试铁三角”的第一条腿,从模糊的概念变成清晰的认知链路。
本文不讲晦涩理论堆砌,而是按照 “问题 → 概念 → 关系 → 示例 → 原理 → 考点” 的递进逻辑,带你一次性把IoC和DI吃透。
一、为什么需要IoC?先看一段“失控”的代码
痛点切入:传统开发中的“new地狱”
假设我们有一个用户注册的业务。初始需求是:用户注册时,把用户信息保存在本地MySQL数据库。
传统写法长这样:
public class UserService { // 硬编码依赖:业务逻辑直接依赖具体实现类 private UserRepository userRepository = new MySQLUserRepository(); public void register(String username) { userRepository.save(username); } }
现在需求变了:要把用户数据迁移到远程MongoDB,或者分布式用户中心。
问题来了:你必须手动修改UserService的源代码,把 new MySQLUserRepository() 改成 new RemoteUserRepository()。-5
随着业务发展,这类问题会越来越多:
A类依赖B类,B类依赖C类和D类……为了拿到A对象,你得先手动new出一整条依赖链,工作量失控;
同一个重量级对象(如HttpClient)在多个类中被重复创建,无法复用,也无法集中配置超时、重试等策略;-5
做单元测试时,无法用Mock对象替换真实依赖;
需求变更时,改一处代码、重编译、重新部署——每一次改动都像在拆地雷。-15
痛点一句话总结:业务代码与具体实现类“焊死”在一起,耦合高、扩展性差、维护困难、测试痛苦。
核心矛盾:代码想要“解耦”,但new关键字在“焊死”
传统开发中,对象什么时候new、在哪里new、怎么new,全由开发者自己决定。你拿到一个A对象,如果它依赖B、C、D……你可能需要额外创建一整个对象网络。-15
这时候你会发现:代码不是在解决问题,而是在管理对象的“血缘关系”。
那么有没有办法,让业务类只依赖抽象接口,而不依赖具体实现类呢?
这就是IoC要解决的问题。
二、核心概念(一):IoC(控制反转)
定义
IoC(Inversion of Control,控制反转) 是一种设计思想。其核心是:对象的创建与依赖关系的管理,不再由程序代码主动控制,而是交给外部容器来完成。-2
翻译成人话就是:把“new对象的权力”从程序员手里交出去。
一句话理解
传统方式:你想要对象?自己new一个。
IoC方式:你想要对象?告诉容器你需要什么,容器给你送过来。
这背后遵循的是著名的 “好莱坞原则” ——“Don‘t call me, I’ll call you.”(别找我们,我们会找你。)-15
类比:餐厅点餐 vs 自己买菜做饭
传统开发 = 你自己去菜市场买菜、洗菜、切菜、炒菜,再端上桌。你要吃一道菜,得先搞定一整条供应链。
IoC模式 = 你坐在餐厅里,告诉服务员“我要一份宫保鸡丁”。你不需要关心鸡是谁杀的、花生是谁剥的、火候怎么控制——餐厅后厨(IoC容器)帮你搞定一切。
IoC就是这套“餐厅后厨”的设计思想:把“做菜”这件事从你手里拿走了。
三、核心概念(二):DI(依赖注入)
定义
DI(Dependency Injection,依赖注入) 是一种设计模式,是IoC的具体实现方式。由容器动态地将依赖关系注入到对象中。-15
通俗理解
IoC说的是“我不管创建了,让别人管”——这是思想。
DI说的是“别人怎么把对象给我”——这是具体做法。
打个比方:
IoC 就像你说:“我不想自己做饭了,交给餐厅。”
DI 就像服务员把做好的菜端到你面前——这就是“注入”的过程。
DI的三种实现方式
Spring提供了三种主要的依赖注入方式:-15
| 注入方式 | 示例 | 特点 |
|---|---|---|
| 构造器注入(推荐) | public UserService(UserDao userDao) { this.userDao = userDao; } | 依赖不可变,便于单元测试,Spring官方首选 |
| Setter注入 | @Autowired public void setUserDao(UserDao userDao) { this.userDao = userDao; } | 可选依赖、可重新配置 |
| 字段注入 | @Autowired private UserDao userDao; | 写法最简洁,但可测试性较差,不推荐生产环境大量使用 |
为什么构造器注入是官方推荐?因为构造器保证了依赖对象在对象创建时就必须存在(不可为null),并且字段可以用final修饰,天然支持不可变性和线程安全。-15
四、IoC与DI的关系:思想 vs 实现
这是面试中最常被问倒的问题,一定要记清楚:
| 维度 | IoC(控制反转) | DI(依赖注入) |
|---|---|---|
| 性质 | 设计思想 / 设计原则 | 设计模式 / 具体实现技术 |
| 角色 | 提出“为什么要这样做” | 回答“具体怎么做到的” |
| 关注点 | 控制权的转移 | 依赖对象的传递方式 |
| 关系 | 目标是IoC,手段是DI | 是实现IoC的具体方式之一 |
一句话概括:IoC是一种“思想”,DI是实现这种“思想”的“技术手段”。 -22
⚠️ 常见误区:
❌ 误以为IoC和DI是“两个不同的东西”——其实它们是对同一件事情的不同角度描述;
❌ 误以为IoC就是Spring特有的——IoC是一种通用设计思想,在其他语言/框架中也有体现;
❌ 答不出“IoC和DI有什么关系”——面试官最爱挖坑的地方,请务必掌握上面的对比表格。
五、代码示例:从“手动new”到“容器注入”的完整演变
场景:UserService 依赖 UserDao
❌ 传统开发(高耦合)
// UserDao接口 public interface UserDao { void queryUser(); } // UserDao实现类 public class UserDaoImpl implements UserDao { @Override public void queryUser() { System.out.println("查询用户信息"); } } // UserService:手动new依赖对象 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强绑定,替换UserDao的实现(如从MySQL切换到Oracle)必须修改UserServiceImpl代码。-6
✅ IoC/DI模式(低耦合)
// UserDao:交给容器管理 @Repository // 声明这是一个Bean,交给IoC容器管理 public class UserDaoImpl implements UserDao { @Override public void queryUser() { System.out.println("查询用户信息"); } } // UserService:依赖由容器注入 @Service // 声明这是一个Bean public class UserServiceImpl implements UserService { // 仅声明依赖,不主动创建——控制权反转 private UserDao userDao; // 构造器注入(推荐方式) @Autowired public UserServiceImpl(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容器——对象的创建、依赖的装配、生命周期的管理,全由容器负责。开发者只需声明“我需要什么依赖”,容器就会自动将依赖“送上门”。-6
对比总结
| 对比维度 | 传统开发 | IoC/DI模式 |
|---|---|---|
| 对象创建 | 开发者手动new | Spring容器自动创建 |
| 依赖关系 | 硬编码在代码中 | 通过配置/注解声明 |
| 耦合度 | 高(与具体实现类绑定) | 低(仅依赖抽象接口) |
| 可测试性 | 差(无法Mock) | 好(可注入Mock对象) |
| 扩展性 | 改需求需改源码 | 替换实现类无需改业务代码 |
六、底层原理:到底是怎么做到的?(反射)
这里需要稍微深入一点。理解了这一层,你的面试答案就能从“背概念”升级为“讲原理”。
IoC容器启动的核心流程
Spring IoC容器的启动,本质上做了三件事:-1
步骤1:加载配置元数据
容器扫描被
@Component、@Service、@Repository、@Controller等注解标记的类;将扫描到的类封装为 BeanDefinition(Bean定义对象)——相当于“Bean的说明书”,包含了类名、是否单例、依赖关系、初始化方法等信息。
步骤2:注册BeanDefinition到容器
容器将BeanDefinition注册到注册表中,本质是一个
Map<String, BeanDefinition>(key是Bean名称,value是Bean定义)。
步骤3:Bean的实例化与依赖注入(核心)
容器根据BeanDefinition创建Bean对象,并完成依赖注入;
这一过程依赖的核心技术就是——Java反射(Reflection)。
反射在这里做了什么?
Java反射允许程序在运行时动态地获取类的信息、动态创建对象、动态调用方法、动态访问/修改属性。
Spring底层正是利用反射来实现DI的:-
实例化:容器通过反射调用类的构造器来创建对象实例(
Constructor.newInstance());依赖注入:容器通过反射获取类中被
@Autowired标记的字段或方法,然后动态地将依赖对象赋值进去(Field.set()或Method.invoke())。
简化版逻辑:
// Spring底层简化示意(非真实源码) // 1. 通过反射创建对象实例 Class<?> clazz = Class.forName("com.example.UserServiceImpl"); Constructor<?> constructor = clazz.getDeclaredConstructor(); Object instance = constructor.newInstance(); // 2. 通过反射注入依赖字段 Field field = clazz.getDeclaredField("userDao"); field.setAccessible(true); // 突破private限制 field.set(instance, userDaoInstance); // 注入依赖对象
一句话总结底层逻辑:
IoC是设计思想,DI是实现方式,而反射是支撑DI得以实现的技术手段。 -5-
扩展:IoC容器的两大核心接口
| 接口 | 定位 | 加载时机 | 日常使用场景 |
|---|---|---|---|
| BeanFactory | 最基础的IoC容器接口 | 懒加载(调用getBean()时才创建) | 嵌入式系统、资源受限环境 |
| ApplicationContext | 增强版容器,继承自BeanFactory | 预加载(启动时创建所有单例Bean) | 绝大多数企业项目(推荐) |
常见实现类:
AnnotationConfigApplicationContext:基于注解配置(最常用)ClassPathXmlApplicationContext:基于XML配置-2
面试加分点:能说出ApplicationContext在启动时会执行refresh()方法,触发配置扫描→Bean定义注册→实例化→初始化的完整流程。-2
七、高频面试题与参考答案
面试题1:什么是IoC?什么是DI?它们之间有什么关系?
参考答案(三层递进结构):
第一层(定义) :IoC(Inversion of Control,控制反转)是一种设计思想,将对象的创建和依赖管理权从代码内部“反转”给外部容器。DI(Dependency Injection,依赖注入)是一种设计模式,是IoC的具体实现方式。
第二层(关系) :IoC是目标,DI是手段。IoC回答“为什么要这样做”(解耦),DI回答“具体怎么做的”(通过构造器、Setter或字段将依赖注入)。
第三层(本质) :它们是对同一件事情的不同角度描述——都是指Spring容器接管了对象的创建和依赖管理,开发者只需声明“需要什么”,无需关心“如何获取”。
面试题2:Spring IoC容器是如何实现依赖注入的?底层用了什么技术?
参考答案:
Spring底层主要依赖Java反射机制来实现依赖注入。流程如下:
容器启动时,扫描带
@Component等注解的类,将其封装为BeanDefinition(Bean的“说明书”);容器通过反射调用构造器创建对象实例;
容器通过反射扫描字段或方法上的
@Autowired注解,将匹配的依赖对象动态注入进去。
一句话概括:IoC是思想,DI是方式,反射是支撑这一切的技术基础。
面试题3:@Autowired和@Resource有什么区别?
参考答案:
| 维度 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring框架自带 | JDK原生(JSR-250规范) |
| 默认匹配策略 | 按类型(byType) | 按名称(byName) |
| 指定名称 | 配合@Qualifier("beanName") | 直接使用@Resource(name="beanName") |
| 适用场景 | Spring项目(推荐) | 需要与Java EE规范兼容时 |
如果容器中存在多个同类型的Bean,使用@Autowired会报错,此时需要用@Qualifier或@Primary解决。-37
面试题4:BeanFactory和ApplicationContext的区别?
参考答案:
BeanFactory是Spring IoC容器的最基础接口,采用懒加载(调用
getBean()时才创建Bean),轻量但功能有限。ApplicationContext继承自BeanFactory,采用预加载(容器启动时创建所有单例Bean),扩展了国际化、事件发布、AOP等企业级功能,是日常开发的首选。
一句话:ApplicationContext是BeanFactory的超集,除非资源极度受限,否则都用ApplicationContext。
面试题5:构造器注入、Setter注入、字段注入,哪种方式最好?为什么?
参考答案:
推荐使用构造器注入,原因如下:
依赖不可变:依赖字段可声明为
final,保证对象创建后依赖不会被修改;防止空指针:构造器注入强制依赖在对象实例化时就必须存在,避免
@Autowired字段为null的风险;便于单元测试:可以通过构造器直接传入Mock对象,无需启动Spring容器;
Spring官方推荐:在Spring 4.x之后的官方文档中,构造器注入被列为首选方式。
字段注入虽然代码简洁,但可测试性差、无法保证依赖不可变,不推荐在核心业务代码中大量使用。
八、结尾总结
核心知识点回顾
IoC(控制反转) :一种设计思想,核心是“把对象的创建权从代码交给容器”,目的是解耦。
DI(依赖注入) :实现IoC的具体技术手段,Spring通过构造器、Setter、字段三种方式将依赖注入到对象中。
IoC与DI的关系:思想 vs 实现——IoC是“目标”,DI是“手段”。
底层原理:Spring依赖Java反射机制,在运行时动态创建对象、注入依赖。
两大容器接口:
BeanFactory(基础,懒加载)和ApplicationContext(增强,预加载)。推荐注入方式:构造器注入 > Setter注入 > 字段注入。
重要提醒
IoC和DI不是两个“不同的东西”,而是同一件事的两个角度。 面试时如果被问到区别,要能说清楚“IoC是思想,DI是实现”这个层次关系,而不是简单说“它们一样”或“它们不同”。
下篇预告
下一篇将深入讲解:
Spring IoC容器的完整生命周期(从实例化到销毁)
BeanPostProcessor扩展点机制循环依赖的解决方案(三级缓存原理)
结合源码进一步理解
refresh()方法的执行流程
📍 本文核心金句(建议收藏记忆):
IoC 是设计思想,DI 是实现方式,反射是技术手段。Spring把这三者组合在一起,才有了我们今天“声明一下就能用”的开发体验。
📌 版权声明:本文基于技术公开资料整理,旨在帮助开发者系统学习Spring IoC/DI核心知识。欢迎转发分享,请保留原文出处。

扫一扫微信交流