Spring 的 IoC/DI 原理:从“我会用”到“我懂它”

小编 2 0

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 的“原始”代码:

java
复制
下载
// 依赖对象:数据访问层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();    }}

这段代码有什么问题?至少三条硬伤

  1. 高耦合UserServiceImplUserDaoImpl 强绑定。想换一种 UserDao 的实现(比如从 MySQL 切换到 Oracle),必须修改 UserServiceImpl 的代码-1

  2. 扩展性差:如果项目中有几十个 Service 对象,每个都需要手动维护依赖关系,代码臃肿不堪;

  3. 可测试性差:单元测试时无法轻松替换 Mock 对象,测试变得极其困难-1

正是这些痛点,催生了 IoC 的设计思想——把“谁来创建对象”这个控制权,从开发者手中交给容器

三、核心概念 A:IoC(控制反转)

标准定义

IoC(Inversion of Control,控制反转) 是一种设计原则,其核心思想是将对象的创建、依赖关系的管理以及生命周期的控制权,从应用程序代码本身转移到外部的容器或框架中-19

拆解关键词

所谓“反转”,对比的是传统的“正转”:

对比维度传统模式(正转)IoC 模式(反转)
谁创建对象开发者手动 newSpring 容器创建
谁管理依赖开发者手动装配容器自动注入
代码特点高耦合、难维护低耦合、易扩展

生活化类比

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、耦合度高、单元测试困难⭐⭐⭐⭐(日常主力,但不推荐生产)

构造器注入示例(最推荐)

java
复制
下载
@Servicepublic class UserService {    private final UserRepository repository;  // final 确保不可变    // Spring 4.3+ 单一构造器可省略 @Autowired    public UserService(UserRepository repository) {        this.repository = repository;    }}

字段注入示例(最常用,但需谨慎)

java
复制
下载
@Servicepublic class ProductService {    @Autowired  // 最简洁,但存在安全隐患    private CategoryService categoryService;}

面试加分回答:生产环境优先使用构造器注入,可选依赖用 Setter 注入,尽量避免字段注入-51

五、概念关系总结

对比维度IoC(控制反转)DI(依赖注入)
本质设计思想 / 设计原则具体实现方式 / 技术手段
解决的问题谁来掌控对象的生命周期?如何把依赖对象传进去?
在 Spring 中的角色宏观的设计目标实现该目标的具体机制
一句话记忆思想:控制权交出去手段:依赖送进来

一句话高度概括:IoC 告诉你“不用自己 new 对象”,DI 告诉 Spring “帮我把它塞进去”。

六、代码示例:让容器帮你干活

下面用注解配置的方式,展示 Spring IoC 容器的完整工作流程:

1. 定义组件(Dao + Service)

java
复制
下载
// 依赖对象:数据访问层 —— 无需手动 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. 从容器中获取并使用

java
复制
下载
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 2BeanDefinition 注册到容器注册表(本质是 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。核心流程是:

  1. 容器启动时扫描配置(XML/注解),将每个 Bean 的信息封装成 BeanDefinition 对象-2

  2. BeanDefinition 注册到容器注册表(本质是一个 Map<String, BeanDefinition>-2

  3. 实例化阶段:容器根据 BeanDefinition 中的全类名,通过反射调用构造器创建 Bean 实例

  4. 依赖注入阶段:容器扫描字段或方法上的 @Autowired 注解,通过反射完成属性赋值-7

  5. 执行初始化回调,Bean 就绪可用。

踩分点:点出“工厂模式+反射”核心组合 + 简述 BeanDefinition 的关键作用 + 说明反射在实例化和注入两处的具体应用。

面试题 4:BeanFactory 和 ApplicationContext 有什么区别?

参考答案

对比维度BeanFactoryApplicationContext
继承关系顶层基础接口继承自 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 容器,控制权被“反转”
DIIoC 的具体实现方式,容器把依赖对象“注入”给目标 Bean
IoC vs DIIoC 是“思想”,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


如果觉得本文有帮助,欢迎点赞、收藏、转发,让更多人看到~