时间戳:2026年4月8日 | 北京

小编头像

小编

管理员

发布于:2026年05月08日

5 阅读 · 0 评论


反射、容器与解耦:彻底搞懂 Spring 的 IoC 与 DI

阅读本文你能获得什么:不再满足于“会用 @Autowired”,而是真正理解 IoC 是什么设计思想、DI 如何落地、底层依赖什么技术、面试怎么答。适合入门 / 进阶学习者、在校学生及面试备考者。

一、写在前面

Spring 是 Java 后端开发的“基石框架”,几乎所有企业级 Java 项目都在用它。而 Spring 之所以能成为如此强大的生态底座,最核心的两个概念就是 IoCDI-1

很多开发者每天都在用 @Autowired@Service,却常常陷入以下困境:

  • 只会用,不懂原理——知道 @Autowired 能注入依赖,但说不清 Spring 是怎么做到的

  • 概念混淆——IoC 和 DI 分不清,面试时答不到点子上

  • 遇到问题束手无策——注入失败、循环依赖不知道从哪里排查

本文将从 问题 → 概念 → 关系 → 示例 → 原理 → 考点 的递进逻辑出发,用通俗语言 + 可运行的代码示例,带你彻底吃透 IoC 与 DI。

💡 本文为系列第一篇,后续将深入探讨 IoC 容器的 Bean 生命周期、AOP 原理以及常见面试题拓展,敬请关注。


二、痛点切入:传统开发到底哪里“痛”?

先看一段代码。假设我们要实现一个用户查询功能,分为两层:数据访问层 UserDao 和服务层 UserService

传统做法(无 IoC):

java
复制
下载
// 数据访问层实现
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();
    }
}

这段代码看起来很简单,但隐藏着致命的三个问题

痛点具体表现
耦合度过高UserServiceImplUserDaoImpl 强绑定。如果将来要从 MySQL 切换到 Oracle 实现,必须直接修改 UserServiceImpl 的源代码-1
扩展性极差新增一个 Dao 实现类时,所有依赖它的服务类都要跟着改,违背“开闭原则”-25
测试困难单元测试时无法 mock UserDao,因为代码里已经写死了 new UserDaoImpl()-25

随着项目规模扩大,对象数量增多,这种“手动 new + 手动维护依赖”的模式会让代码变得臃肿不堪,维护成本呈指数级增长。

那有没有办法把这些“脏活累活”交给别人统一处理? 这就是 IoC 要解决的问题。


三、核心概念:IoC——控制反转

3.1 标准定义

IoC(Inversion of Control,控制反转) 是一种设计思想,其核心是:对象的创建权、依赖的装配权,从业务逻辑代码中转移到外部容器,由容器统一管理-1

3.2 拆解关键词

  • 控制:指对象的创建和依赖关系的管理

  • 反转:相对于传统“主动创建”而言,把控制权从开发者“反转”给框架/容器

  • 本质判断:判断一个类是否实现了 IoC,只需要看一个标准——对象的创建时机和依赖来源,是否由该对象自身决定。如果 A 类里直接 new B(),那 A 控制着 B 的实例化;如果 A 的构造函数接收一个 B 实例(不管是谁传进来的),控制权就移交出去了-11

3.3 生活化类比

“组织家庭聚餐” 来类比最贴近日常-6

  • 传统开发模式(自己办聚餐):你要自己列食材清单(确定依赖),自己去超市采购(new 对象),自己洗菜备菜做菜(关联依赖)。少买一样东西,菜就做不出来。

  • IoC 模式(找上门厨师):你只需要告诉厨师“周末中午 10 人聚餐,要 3 个热菜、2 个凉菜”(声明需求)。厨师会自己列清单、采购食材、做菜上桌——你完全不用关心食材怎么来的、依赖怎么配的。

IoC 的核心价值不是“少写几行 new 代码”,而是实现真正的解耦。 就像聚餐时你和菜场解耦,不用关心菜场在哪、食材多少钱;代码中,对象和对象的创建逻辑解耦,换一个实现类时不需要去改所有用到它的代码-6


四、关联概念:DI——依赖注入

4.1 标准定义

DI(Dependency Injection,依赖注入) 是指:一个类所依赖的对象不由其自身创建,而是由外部容器创建并注入到该类中,从而实现类与类之间的解耦-25

4.2 拆解“依赖”与“注入”

  • 依赖:如果类 A 需要调用类 B 的方法来完成业务逻辑,那么“类 A 依赖于类 B”-25

  • 注入:容器在创建对象时,自动把该对象需要的依赖“送进去”,不需要开发者手动关联

4.3 DI 的三种实现方式

Spring 支持三种依赖注入方式,各有适用场景-19

注入方式写法示例特点推荐程度
构造器注入public UserService(UserDao userDao)依赖不可缺失,对象创建时就确定,最利于测试⭐⭐⭐ 最推荐
Setter 注入public void setUserDao(UserDao userDao)依赖可选,灵活性高,但容易遗漏⭐⭐ 按需使用
字段注入@Autowired private UserDao userDao最简洁,但反射开销较大,不利于单元测试⭐ 不推荐生产使用

为什么构造器注入最推荐? 因为它能确保依赖不为空——对象在创建时就必须传入所有必要的依赖,避免后续使用时才发现“少了食材”-6

4.4 生活化类比(接上文的聚餐例子)

  • 构造器注入:厨师必须先拿到鸡翅才能开始做可乐鸡翅——依赖不可缺

  • Setter 注入:厨师可以先做其他菜,等可乐送到了再做鸡翅——依赖可选

  • 字段注入:厨师直接用水池边备好的食材,不用自己去取——最省事但最不透明-6


五、IoC 与 DI 的关系:一句话记住

维度IoC(控制反转)DI(依赖注入)
本质设计思想实现方式
作用定义“控制权转移”的哲学描述“如何把依赖送进去”的具体动作
类比聚餐时“让厨师全权负责”这个想法厨师把可乐倒进鸡翅锅这个具体动作

一句话记忆:IoC 是一种设计思想,DI 是这种思想的具体实现方式--23

💡 补充一个小知识点:IoC 有两种实现方式——依赖查找(DL)依赖注入(DI)。DI 是目前最主流的实现方式-1


六、代码示例:从“痛”到“通”的完整改造

6.1 用 Spring IoC/DI 改造上述代码

java
复制
下载
// 1️⃣ 依赖对象:UserDao(无需手动 new)
@Repository
public class UserDaoImpl implements UserDao {
    @Override
    public void queryUser() {
        System.out.println("查询用户信息");
    }
}

// 2️⃣ 目标对象:UserService(依赖由容器注入)
@Service
public class UserServiceImpl implements UserService {
    // 仅声明依赖,不主动创建
    private UserDao userDao;
    
    // 构造器注入——最推荐的方式
    @Autowired
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    public void queryUser() {
        userDao.queryUser();
    }
}

// 3️⃣ 测试类:从容器中获取对象,无需手动管理依赖
public class Test {
    public static void main(String[] args) {
        // 容器初始化——Spring 自动创建 Bean、装配依赖
        ApplicationContext context = 
            new AnnotationConfigApplicationContext(AppConfig.class);
        // 直接获取对象,依赖已自动注入
        UserService userService = context.getBean(UserService.class);
        userService.queryUser();
    }
}

6.2 执行流程解读

  1. ApplicationContext 容器启动,读取配置元数据(@Configuration 或包扫描路径)

  2. Spring 扫描到 @Repository@Service 注解的类,将它们注册为 Bean

  3. 容器发现 UserServiceImpl 的构造器上有 @Autowired,知道它需要 UserDao

  4. 容器先创建 UserDaoImpl 实例,然后将其注入到 UserServiceImpl 的构造器中

  5. 开发者从容器中 getBean(),拿到的 UserService 已经是一个完整装配好的对象

核心变化对比:

对比项传统做法Spring IoC/DI 做法
谁创建对象开发者手动 newSpring 容器自动创建
谁管理依赖开发者手动维护Spring 容器自动注入
更换实现类需要修改源码改配置或注解即可
单元测试难以 mock轻松注入 mock 对象

七、底层原理:反射 + 容器体系

7.1 技术支撑——反射

Spring IoC 容器之所以能“自动创建对象”,底层依赖的核心技术是 Java 反射机制

反射允许程序在运行时获取类的构造器、方法、字段等元信息,并动态创建实例。Spring 通过类的全限定名(如 "com.example.UserService")拿到字节码信息,然后用 Constructor.newInstance() 动态创建对象,而不是写死在代码里的 new-6-11

💡 一句话理解反射:让代码在运行时“自己看说明书造自己”,而不是在编译时就写好“我要怎么造”。

7.2 IoC 容器的两大核心接口

Spring 的 IoC 容器底层是一套接口体系,最核心的是两个-4-4

接口特点使用场景
BeanFactory懒加载(只有调用 getBean() 时才创建 Bean)、功能基础、轻量嵌入式设备、资源受限环境
ApplicationContext预加载(启动时创建所有单例 Bean)、功能完整(支持 AOP、事件、国际化等)99% 的企业项目(默认选择)

日常开发中,我们几乎都在使用 ApplicationContext 的各类实现,比如 AnnotationConfigApplicationContext(注解配置)和 ClassPathXmlApplicationContext(XML 配置)-4

7.3 IoC 容器的核心执行流程

以注解配置为例,Spring 容器从启动到创建 Bean 的全流程大致如下-4

图表
代码
下载
全屏
.kvfysmfp{overflow:hidden;touch-action:none}.ufhsfnkm{transform-origin: 0 0}
mermaid-svg-6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}mermaid-svg-6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}mermaid-svg-6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}mermaid-svg-6 .error-icon{fill:552222;}mermaid-svg-6 .error-text{fill:552222;stroke:552222;}mermaid-svg-6 .edge-thickness-normal{stroke-width:1px;}mermaid-svg-6 .edge-thickness-thick{stroke-width:3.5px;}mermaid-svg-6 .edge-pattern-solid{stroke-dasharray:0;}mermaid-svg-6 .edge-thickness-invisible{stroke-width:0;fill:none;}mermaid-svg-6 .edge-pattern-dashed{stroke-dasharray:3;}mermaid-svg-6 .edge-pattern-dotted{stroke-dasharray:2;}mermaid-svg-6 .marker{fill:333333;stroke:333333;}mermaid-svg-6 .marker.cross{stroke:333333;}mermaid-svg-6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}mermaid-svg-6 p{margin:0;}mermaid-svg-6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:333;}mermaid-svg-6 .cluster-label text{fill:333;}mermaid-svg-6 .cluster-label span{color:333;}mermaid-svg-6 .cluster-label span p{background-color:transparent;}mermaid-svg-6 .label text,mermaid-svg-6 span{fill:333;color:333;}mermaid-svg-6 .node rect,mermaid-svg-6 .node circle,mermaid-svg-6 .node ellipse,mermaid-svg-6 .node polygon,mermaid-svg-6 .node path{fill:ECECFF;stroke:9370DB;stroke-width:1px;}mermaid-svg-6 .rough-node .label text,mermaid-svg-6 .node .label text,mermaid-svg-6 .image-shape .label,mermaid-svg-6 .icon-shape .label{text-anchor:middle;}mermaid-svg-6 .node .katex path{fill:000;stroke:000;stroke-width:1px;}mermaid-svg-6 .rough-node .label,mermaid-svg-6 .node .label,mermaid-svg-6 .image-shape .label,mermaid-svg-6 .icon-shape .label{text-align:center;}mermaid-svg-6 .node.clickable{cursor:pointer;}mermaid-svg-6 .root .anchor path{fill:333333!important;stroke-width:0;stroke:333333;}mermaid-svg-6 .arrowheadPath{fill:333333;}mermaid-svg-6 .edgePath .path{stroke:333333;stroke-width:2.0px;}mermaid-svg-6 .flowchart-link{stroke:333333;fill:none;}mermaid-svg-6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}mermaid-svg-6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}mermaid-svg-6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}mermaid-svg-6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}mermaid-svg-6 .cluster rect{fill:ffffde;stroke:aaaa33;stroke-width:1px;}mermaid-svg-6 .cluster text{fill:333;}mermaid-svg-6 .cluster span{color:333;}mermaid-svg-6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid aaaa33;border-radius:2px;pointer-events:none;z-index:100;}mermaid-svg-6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:333;}mermaid-svg-6 rect.text{fill:none;stroke-width:0;}mermaid-svg-6 .icon-shape,mermaid-svg-6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}mermaid-svg-6 .icon-shape p,mermaid-svg-6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}mermaid-svg-6 .icon-shape rect,mermaid-svg-6 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}mermaid-svg-6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}mermaid-svg-6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}mermaid-svg-6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
1. 容器初始化:加载配置元数据
2. 扫描注解,封装BeanDefinition
3. 注册BeanDefinition到注册表Map
4. Bean实例化:反射调用构造器
5. 属性填充:依赖注入
6. 初始化回调
7. Bean就绪,可供使用

核心要点:

  • BeanDefinition:Spring 把每个 Bean 的信息(类名、是否单例、依赖关系等)封装成一个“说明书”对象,相当于 Bean 的设计图纸-4

  • 反射:贯穿整个实例化过程的关键技术,让 Spring 能够动态创建对象、动态调用方法


八、高频面试题与参考答案

Q1:什么是 IoC?什么是 DI?两者的关系是什么?

回答框架:概念定义 → 关系阐述 → 生活化类比

参考答案:

  • IoC(控制反转) 是一种设计思想,核心是把对象的创建权、依赖的装配权从代码转移到外部容器,由容器统一管理

  • DI(依赖注入) 是实现 IoC 思想的一种具体技术手段,指容器在创建对象时,自动将该对象需要的依赖对象注入进去

  • 关系:IoC 是“思想”,DI 是“实现”;IoC 回答“谁来管”,DI 回答“怎么给”

  • 类比:IoC 好比“找厨师来操办聚餐”这个想法,DI 好比“厨师把可乐倒进鸡翅锅”这个具体动作

Q2:Spring 中 BeanFactory 和 ApplicationContext 有什么区别?

回答框架:定位对比 → 关键差异 → 使用建议

参考答案:

对比维度BeanFactoryApplicationContext
加载时机懒加载(调用 getBean() 时才创建)预加载(容器启动时创建所有单例 Bean)
功能范围仅提供基础的 IoC 能力(getBean() 等)继承 BeanFactory,额外支持 AOP、国际化、事件发布、资源加载
使用场景嵌入式系统、资源受限环境绝大多数企业项目(推荐使用)

ApplicationContextBeanFactory 的超集,除非有特殊限制,否则永远使用 ApplicationContext

Q3:Spring DI 有哪些注入方式?推荐使用哪种?

回答框架:列举三种 → 对比优劣 → 给出推荐

参考答案:

  • 构造器注入(最推荐):依赖不可变,对象创建时即确定依赖,最利于单元测试和避免空指针

  • Setter 注入:依赖可选,灵活性高,但容易遗漏导致空指针

  • 字段注入@Autowired 直接写在字段上):写法最简洁,但反射开销较大,不利于测试,不推荐生产使用

Q4:Spring IoC 的底层原理是什么?

回答框架:核心组件 → 关键技术 → 核心流程

参考答案:

Spring IoC 底层通过 工厂模式 + 反射机制 实现-。核心流程如下:

  1. 容器启动时加载配置元数据(XML/注解/Java 配置)

  2. 将扫描到的类封装为 BeanDefinition(Bean 的“说明书”)

  3. 将 BeanDefinition 注册到容器内部的注册表(本质是一个 Map<String, BeanDefinition>

  4. 容器通过 反射 调用构造器实例化 Bean

  5. 根据依赖关系,通过反射完成属性填充(依赖注入)

  6. 执行初始化回调,Bean 就绪可供使用

Q5:Spring 如何解决循环依赖?

这是中高级面试常问的扩展题,提前为你做预告和铺垫

简要回答:Spring 通过 三级缓存 机制解决单例 Bean 的构造器循环依赖问题,但构造器注入的循环依赖无法解决,会直接抛出异常。具体原理将在系列后续文章中深入讲解。


九、结尾总结

9.1 核心知识点回顾

核心要点一句话总结
IoC 是什么一种设计思想:对象的创建权交给容器,开发者只关心业务逻辑
DI 是什么实现 IoC 的具体手段:容器自动把依赖对象“注入”进来
两者关系IoC 是思想,DI 是实现
为什么要用解耦、易测试、易扩展
底层原理反射 + BeanDefinition + 容器接口体系
推荐注入方式构造器注入

9.2 重点提醒

⚠️ 最容易踩的坑

  • 手动 new 对象,绕过了 Spring 容器,导致 @Autowired 字段为 null-11

  • Bean 未被扫描:确保 Bean 类上有正确的注解(@Component/@Service/@Repository 等),且位于 Spring Boot 主类所在包或子包下-24

  • 多个同类型 Bean 导致注入失败:使用 @Primary@Qualifier 指定

9.3 下篇预告

本文重点梳理了 IoC 与 DI 的核心概念、关系、代码示例和底层原理。下一篇将深入探讨:

  • Bean 的完整生命周期——从实例化到销毁的全过程

  • 循环依赖的解决机制——Spring 的三级缓存如何工作

  • AOP 与 IoC 的协同——代理对象的创建与管理

如果觉得本文对你有帮助,欢迎点赞、收藏、转发!有任何疑问或想深入了解的话题,欢迎在评论区留言讨论。


本文最后更新于 2026 年 4 月 8 日,内容基于 Spring Framework 当前主流的实践和原理整理。随着 Spring Boot 3.x 的普及,部分实现细节可能略有变化,但核心思想与原理保持稳定。

标签:

相关阅读