发布于:北京时间 2026年4月10日 | 阅读时长:约8分钟
一、开篇:为什么这篇文章值得你看?

在Java后端技术体系中,Spring IoC(Inversion of Control,控制反转)与DI(Dependency Injection,依赖注入) 是无可争议的“必修课”——几乎所有企业级项目都在用,面试几乎必问,但真正能把两者关系说清楚、把底层原理讲明白的人,却并不多。
如果你是一个正在学习Spring的技术入门者,可能遇到过这样的困扰:

代码里天天写
@Autowired,知道它能“自动注入”,但问起“底层怎么实现的”,一脸茫然;面试官问“IoC和DI有什么区别”,你回答“IoC就是DI”,然后看到对面表情逐渐凝固;
读源码时,看到
BeanFactory和ApplicationContext两个容器,搞不清谁是谁,更不明白Bean的生命周期到底是谁在管。
这些问题,本质上都不是因为你不够努力,而是因为概念堆砌太多、逻辑链条断裂——大多数人只记住了结论,却不知道结论从何而来。
本文围绕 Spring IoC与DI 展开,从痛点切入 → 概念拆解 → 关系梳理 → 代码对比 → 底层原理 → 面试要点,帮你建立一条完整清晰的知识链路。本文不是八股文堆砌,而是试图让你“看得懂、记得住、面试能用上”。
二、痛点切入:为什么需要IoC与DI?
先看一段代码,这是很多初学者甚至不少生产项目里都能见到的写法:
public class OrderService { // 直接在类内部new依赖对象 —— 硬编码 private UserRepository userRepository = new UserRepositoryImpl(); private PaymentClient paymentClient = new AlipayClient(); public void createOrder(Order order) { userRepository.save(order.getUser()); paymentClient.pay(order.getAmount()); } }
这段代码有什么问题?看起来很直观,业务逻辑也跑得通。但如果你站在维护和扩展的角度看,缺陷非常明显:
第一,耦合度过高。 OrderService直接依赖UserRepositoryImpl和AlipayClient的具体实现。有一天产品经理说要换支付渠道——从支付宝换成微信支付,你就得去改OrderService里的new AlipayClient()。改一个地方还好,如果这个支付逻辑散布在十几个Service里,就是一场噩梦。
第二,单元测试极其困难。 想单独测试OrderService的业务逻辑?做不到。因为new UserRepositoryImpl()会真实连数据库,new AlipayClient()会发起真实网络请求。测试环境里数据被改乱了、真金白银被扣了,谁来负责?
第三,代码复用性差。 如果UserRepositoryImpl的实现从MySQL换成Redis,所有new了它的类都得改。
第四,生命周期管理混乱。 每个对象什么时候创建、什么时候销毁,完全由业务代码自己决定。你想让PaymentClient是单例?做不到。想让它随着容器关闭而释放资源?也做不到。
这些问题归结起来就是一句话:对象由谁创建、依赖由谁管理——这件事不应该由业务类自己决定。
这恰恰就是IoC(控制反转)和DI(依赖注入)要解决的命题:把对象的创建权和管理权,从业务代码手中“反转”给一个统一的容器。
三、核心概念讲解:什么是IoC(控制反转)?
3.1 标准定义
IoC全称 Inversion of Control,中文译作控制反转。它是一种设计原则或架构思想,而非某种具体的技术实现--。
3.2 拆解关键词
所谓“控制”,指的是对象的创建、依赖关系的建立、生命周期的管理这些权限。所谓“反转”,就是把这些控制权从应用程序代码内部,反转给外部的容器或框架。
在传统编程中,当A类需要使用B类的功能时,A会在内部直接new B()——此时A完全掌控B的创建时机、方式和生命周期-40。引入IoC后,A不再负责创建B,只声明“我需要一个B”,由外部容器统一管理B的实例化与供给-。
3.3 生活化类比
想象一下你下班回家做饭的两种模式:
传统模式:你得亲自去超市买菜(创建依赖)、洗菜切菜(准备依赖)、炒菜做饭(使用依赖)。每一步都要自己动手,耗时耗力,想换菜系还得重新学。
IoC模式:你去餐厅点餐,只需要告诉服务员“我要一份宫保鸡丁”(声明依赖),厨师(容器)负责买菜、备料、烹饪。你完全不用关心食材从哪来、怎么做,只管享用成果-39。
3.4 IoC解决的核心问题
IoC解决了传统开发中最头疼的耦合问题。它让代码从“主动控制”变为“被动接收”,依赖关系的管理从业务代码中剥离出来,从而显著提升了代码的可维护性、可测试性和可扩展性-10-29。
四、关联概念讲解:什么是DI(依赖注入)?
4.1 标准定义
DI全称 Dependency Injection,中文译作依赖注入。它是一种具体的设计模式,是实现IoC原则最主流、最成功的技术手段--39。
4.2 DI要解决的问题
IoC讲的是“谁来控制”,DI讲的则是“怎么把依赖传进去”-40。DI专注于解决一个问题:如何将一个对象所依赖的其他对象,“注入”到该对象的内部。
4.3 依赖注入的三种常见方式
在Spring框架中,DI主要通过以下三种方式实现-29-30:
(1)构造器注入(Constructor Injection) —— 通过类的构造函数传入依赖:
@Service public class OrderService { private final UserRepository userRepository; private final PaymentClient paymentClient; // Spring 4.3+ 后,如果只有一个构造器,可省略 @Autowired public OrderService(UserRepository userRepository, PaymentClient paymentClient) { this.userRepository = userRepository; this.paymentClient = paymentClient; } }
特点:依赖不可变(final),对象创建后依赖就位,最适合单元测试。Spring官方推荐使用-。
(2)Setter注入 —— 通过setter方法传入依赖:
@Service public class OrderService { private UserRepository userRepository; @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } }
特点:允许可选依赖,支持动态替换,但破坏了不可变性。
(3)字段注入(Field Injection) —— 直接在字段上使用注解:
@Service public class OrderService { @Autowired private UserRepository userRepository; }
特点:写法最简洁,但不利于单元测试(无法在不启动容器的情况下替换依赖),且依赖为final时无法使用-45。
4.4 DI的运行机制简述
当Spring容器启动时,它会扫描所有被@Component、@Service、@Repository、@Controller等注解标记的类,或者通过@Bean方法注册的对象,将它们作为Bean注册到容器中。当某个Bean声明了对另一个Bean的依赖(如@Autowired标注的字段),容器会在实例化该Bean后,自动去容器中查找匹配的目标Bean,并通过反射完成赋值--10。
五、概念关系与区别总结
IoC和DI的关系,是很多面试者最容易栽跟头的地方。
IoC是思想:一种高层的设计原则,回答的是 “谁来控制” ——控制权从应用程序代码转移到外部容器-40。
DI是手段:一种具体的技术实现,回答的是 “怎么传递依赖” ——通过构造器、setter或字段注入等方式-40。
一句话总结:IoC是“目标”,DI是“路径”;IoC是“设计哲学”,DI是“落地代码”。
两者不是对等关系,而是包含与被包含的关系。可以用一个简单的公式来记:
IoC = 控制权的反转(设计原则)
DI = IoC的主流实现方式(技术手段)
-39-
六、代码示例对比:直观感受IoC+DI带来的改变
6.1 传统写法(无IoC/DI)
// 紧耦合,难以测试和维护 public class OrderService { private UserRepository userRepo = new UserRepositoryImpl(); private PaymentClient paymentClient = new AlipayClient(); public void createOrder(Order order) { userRepo.save(order.getUser()); paymentClient.pay(order.getAmount()); } }
6.2 Spring IoC+DI写法(推荐)
// 1. 定义接口 public interface PaymentClient { void pay(BigDecimal amount); } // 2. 实现类,交给Spring管理 @Service public class AlipayClient implements PaymentClient { @Override public void pay(BigDecimal amount) { System.out.println("调用支付宝支付:" + amount); } } // 3. 业务类:声明依赖,不负责创建 @Service public class OrderService { private final UserRepository userRepository; private final PaymentClient paymentClient; // 构造器注入(Spring推荐) public OrderService(UserRepository userRepository, PaymentClient paymentClient) { this.userRepository = userRepository; this.paymentClient = paymentClient; } public void createOrder(Order order) { userRepository.save(order.getUser()); paymentClient.pay(order.getAmount()); // 实际调用的是AlipayClient } }
6.3 改进效果对比
| 维度 | 传统写法 | Spring IoC+DI写法 |
|---|---|---|
| 更换支付渠道 | 修改所有new的地方 | 替换实现类,其他地方零改动 |
| 单元测试 | 真实调用数据库/网络 | 注入Mock对象,纯业务逻辑测试 |
| 依赖关系 | 硬编码,无法复用 | 通过接口解耦,灵活替换 |
| 生命周期 | 业务代码自行管理 | 容器统一管理(单例、初始化、销毁) |
关键代码行解释:在OrderService的构造器中,UserRepository userRepository和PaymentClient paymentClient并非由OrderService自己创建,而是由Spring容器在启动时自动查找匹配的Bean并“注入”进来。这就是DI的本质。而OrderService不再关心“谁来创建这些对象”,只关心“我如何使用它们”——这正是控制权的反转(IoC)。
七、底层原理与技术支撑
7.1 反射机制是IoC/DI的底层基石
Spring IoC容器之所以能在运行时动态创建对象、注入属性,依赖的核心Java技术就是反射(Reflection)-44。
具体流程如下:
加载阶段:Spring扫描配置(XML、注解或Java Config),读取类的全限定名。
实例化阶段:通过
Class.forName(className).newInstance()或构造器的newInstance()方法,利用反射动态创建Bean实例——整个过程完全不需要代码中出现new关键字-44-29。注入阶段:当遇到
@Autowired标注的字段时,Spring通过反射找到该字段,再从容器中查找匹配的目标Bean,通过Field.set(instance, value)完成赋值。
7.2 IoC容器的两种核心实现
Spring的IoC容器主要由两个核心接口构成-10-20:
| 容器 | 特点 | 使用场景 |
|---|---|---|
| BeanFactory | Spring的基础设施,提供最基本的IoC支持,延迟加载(使用时才创建) | 资源受限的轻量级场景 |
| ApplicationContext | BeanFactory的子接口,继承所有功能并扩展了国际化、事件传播、资源加载等企业级服务,预加载(启动时就创建所有单例Bean) | 几乎所有的生产项目(Spring Boot默认容器) |
7.3 Bean的生命周期——容器的“核心调度能力”
Spring IoC容器是Bean生命周期的唯一总控方,完整的管理流程为:实例化 → 属性赋值 → 初始化 → 使用 → 销毁-20-。
开发者作为“参与者”,只能在容器开放的点位进行定制:
| 阶段 | 开发者可介入的扩展点 |
|---|---|
| 容器启动 | BeanFactoryPostProcessor(修改Bean定义) |
| 实例化后、注入前 | Aware系列接口(BeanNameAware、ApplicationContextAware等) |
| 初始化前后 | BeanPostProcessor(AOP代理生成等在此完成) |
| 初始化阶段 | @PostConstruct、InitializingBean.afterPropertiesSet()、自定义init-method |
| 销毁阶段 | @PreDestroy、DisposableBean.destroy()、自定义destroy-method |
-20-
需要特别说明:本文聚焦于IoC与DI的核心概念、关系、原理和面试要点,属于Spring底层原理的“地基”部分。关于循环依赖的三级缓存机制、BeanPostProcessor实现AOP代理的底层细节、以及Spring Boot自动配置原理等内容,限于篇幅,将在后续系列文章中展开,敬请关注。
7.4 一句话总结底层原理
Spring IoC容器的本质是:通过反射+容器(Map),在运行时动态管理对象的创建和依赖关系。反射提供了“在编译期不知道类名的情况下也能创建对象”的能力,容器(Map)提供了“在全局范围内唯一标识和定位Bean”的能力,两者结合构成了IoC/DI的技术底座。
八、高频面试题与参考答案
以下题目为Spring IoC/DI方向的高频面试题,覆盖概念理解、原理追问和场景应用三个层次。
面试题1:请谈谈你对Spring IoC的理解?它解决了什么问题?
参考答案要点:
第一步:下定义。 IoC是Inversion of Control的缩写,即控制反转,是Spring框架的核心设计原则-29。
第二步:说“反转”的是什么。 它将对象的创建权、依赖关系的管理权、生命周期的控制权,从应用程序代码内部“反转”给外部的Spring IoC容器-30。
第三步:点出解决的问题。 传统new对象的方式会导致代码紧耦合、单元测试困难、难以扩展。IoC通过将控制权交给容器,实现了模块间的解耦,提升了代码的可测试性和可维护性--10。
第四步:举一个典型场景。 例如更换支付渠道时,传统写法需要修改所有new了支付实现的地方;而基于IoC,只需在容器中更换Bean的实现,业务代码零改动。
踩分点:定义+反转对象+解决耦合+举例说明,四步完整不缺。
面试题2:IoC和DI有什么区别和联系?它们是一回事吗?
参考答案要点:
核心结论:不是一回事。
IoC(控制反转) 是一种设计思想或原则,回答的是 “谁来控制” ——控制权从应用程序代码转移给外部容器-40。
DI(依赖注入) 是一种具体的设计模式,回答的是 “怎么传递依赖” ——通过构造器、Setter或字段等方式将依赖对象注入-40。
二者的关系:DI是实现IoC的最主流方式。IoC是“目标”,DI是“路径”。用一句话概括:IoC是思想,DI是思想的具体实现-39-。
踩分点:明确区分思想vs手段、分别给出定义、说明关系、一句话总结,四层逻辑清晰。
面试题3:Spring中的Bean生命周期包含哪些阶段?开发者在哪些节点可以介入?
参考答案要点:
Spring Bean的生命周期包含四大核心阶段:实例化 → 属性赋值(依赖注入)→ 初始化 → 销毁-。
开发者可以介入的扩展节点包括:
容器启动阶段:
BeanFactoryPostProcessor(修改Bean定义元信息)实例化之后、注入之前:Aware系列接口(
ApplicationContextAware等)初始化前后:
BeanPostProcessor(AOP代理生成在此完成)初始化阶段:
@PostConstruct、InitializingBean.afterPropertiesSet()、自定义init-method销毁阶段:
@PreDestroy、DisposableBean.destroy()、自定义`destroy-method》
一个易错点:依赖注入(DI)完成后才会触发@PostConstruct等初始化回调。如果@Autowired字段没有成功注入,在@PostConstruct方法中访问该字段会抛出NullPointerException-21。
踩分点:四阶段顺序完整、扩展点能列举3个以上、指出DI与初始化的依赖关系,加分。
面试题4:Spring IoC容器底层是怎么创建对象的?不用new怎么做到的?
参考答案要点:
Spring IoC容器底层依赖Java反射机制(Reflection) 实现对象的动态创建-44。
具体流程:
Spring扫描配置(XML/注解/Java Config),读取类的全限定名
通过
Class.forName(className)获取类的Class对象调用
clazz.getDeclaredConstructor().newInstance()(反射方式)动态创建实例遇到
@Autowired标注的字段时,通过Field.set(instance, value)完成注入
整个过程完全不需要在代码中写new关键字,Spring通过反射 + 容器(Map存储Bean实例)实现了解耦-29-44。
踩分点:指出反射技术、简述三步流程、提到容器Map存储,展示底层理解。
面试题5:BeanFactory和ApplicationContext有什么区别?
参考答案要点:
BeanFactory是Spring IoC容器的基础接口,提供最基本的IoC功能。特点是延迟加载(懒加载)——只有在调用getBean()时才会创建Bean实例-20。
ApplicationContext是BeanFactory的子接口,除了继承BeanFactory的所有功能外,还增加了国际化支持、事件传播机制、资源加载等企业级服务。特点是预加载(饿加载)——容器启动时就创建所有单例Bean,运行时直接使用-20-10。
在实际开发中,99%的场景使用ApplicationContext(Spring Boot默认容器),因为它功能更完善、性能预热更充分。
踩分点:父接口vs子接口、懒加载vs预加载、功能差异、实战选择。
九、总结回顾
至此,我们已经完整梳理了Spring IoC与DI的知识体系。回顾全文,你需要牢牢记住以下核心知识点:
IoC(控制反转) :一种设计思想,将对象的创建权、依赖管理权、生命周期控制权从应用程序代码“反转”给外部容器,核心价值是解耦。
DI(依赖注入) :实现IoC的具体技术手段,通过构造器、Setter、字段三种方式将依赖对象注入到目标类中。
二者关系:IoC是思想(回答“谁来控制”),DI是手段(回答“怎么传递”)。IoC是目标,DI是路径。
底层原理:反射机制 + 容器Map,在运行时动态创建对象和管理依赖关系。
Bean生命周期:实例化 → 属性赋值 → 初始化 → 销毁,开发者通过扩展点介入。
容器实现:
BeanFactory(基础、懒加载)vsApplicationContext(增强、预加载)。
学习建议:IoC和DI是Spring生态的基石,理解透彻后,再去学习AOP、事务管理、Spring Boot自动配置等高级特性会事半功倍。如果你还没有亲手写过基于构造器注入的代码,建议现在就动手——一个@Service、一个@Autowired、一个单元测试,足以让你直观感受IoC/DI带来的改变。
下一篇文章我们将深入Spring AOP的实现原理,讲解动态代理(JDK动态代理 vs CGLIB)的底层机制,以及@Transactional注解为什么有时不生效的问题。欢迎持续关注。
🔗 本文为Spring底层原理系列第一篇。如需转载,请注明出处。如果觉得文章对你有帮助,欢迎点赞、收藏、转发支持。