Spring IoC 与 DI 核心:IoC 是思想,DI 是手段,附面试要点

小编 2 0

本文基于 2026-04-10 的最新资料,深入解析 Spring 的两大核心机制——IoC 与 DI。

开篇引入

Spring 框架自诞生以来,始终以“轻量级、非侵入、面向接口编程”为信条,在 Java 企业级开发中占据不可撼动的地位。而这一切的根基,正是两个看似朴素却极具颠覆性的思想:IoC(控制反转)与 DI(依赖注入)。前者将对象创建的权力从程序员手中交给容器,后者则是实现这一转变的具体手段-5

不少开发者对 IoC 与 DI 的认知停留在“会用 @Autowired”层面,一旦被问到“两者有什么区别”“Spring 如何解决循环依赖”,便无从答起。这恰恰是面试中高频出现的扣分点

本文将从为什么要用 → 概念解析 → 代码示例 → 底层原理 → 面试要点这条主线展开,带你一次性彻底搞懂 Spring IoC 与 DI。

一、痛点切入:为什么需要 IoC?

传统开发方式

在传统开发中,当我们需要使用一个对象时,直接在代码里用 new 关键字创建它:

java
复制
下载
public class OrderService {
    // 硬编码依赖
    private PaymentService payment = new AlipayService();
    private Logger logger = new FileLogger("/tmp/log");

    void pay() {
        payment.process();
    }
}

传统方式的致命缺陷

  • 紧耦合:想将支付宝换成微信支付,必须修改 OrderService 源代码,重新编译部署;

  • 难以测试:无法轻松替换为 Mock 对象进行单元测试;

  • 维护成本高:一个对象可能依赖另一个对象,后者又依赖第三个……为了拿到对象 A,可能要手动创建一串依赖链-9

IoC 的设计初衷

为了打破这种困局,软件设计领域提出了 控制反转(IoC) 这一设计原则。IoC 的核心思想是:将对象创建、管理的控制权从应用程序代码转移给外部容器,由容器统一调度、管理与销毁-5

二、IoC:控制反转

标准定义

IoC(Inversion of Control,控制反转) 是一种设计思想,指的是将原本在程序中手动创建对象的控制权,交给第三方(如 IoC 容器)来管理-29

核心拆解

  • 控制:对象创建(实例化)、生命周期管理、依赖管理的权力;

  • 反转:这种权力从程序员代码移交给了外部容器-29

生活化类比

  • 传统方式:就像自己在家做饭。你需要主动去超市买菜、洗菜、切菜、炒菜,整个过程完全由你控制-1

  • IoC 方式:就像去餐厅吃饭。你只需要点菜(声明需要什么),厨师(IoC 容器)负责把菜做好送到你面前-1。你不需要关心菜是怎么做的,只管享用。

IoC 解决了什么问题?

  1. 降低耦合度:对象之间不再直接依赖,而是依赖容器;

  2. 资源易于管理:如单例、作用域控制由容器统一处理;

  3. 提升可测试性:依赖可以被轻松替换为 Mock 对象-29

三、DI:依赖注入

标准定义

DI(Dependency Injection,依赖注入) 是一种设计模式,是 IoC 的具体实现方式,由容器动态地将依赖关系注入到对象中-9

DI 的三种实现方式

Spring 主要提供了三种依赖注入方式:

注入方式实现方式推荐度关键特征
构造器注入通过构造函数参数注入✅ 最推荐依赖不可变、强制注入
Setter 注入通过 Setter 方法注入可选场景适合可选依赖
字段注入通过 @Autowired 直接注入字段⚠️ 不推荐生产写法最简洁但易出 NPE

构造器注入(推荐方式) :Spring 官方首选,依赖声明为 final,确保对象一旦创建就拥有完整依赖,避免运行时 NPE-9-23

java
复制
下载
@Service
public class UserService {
    private final UserRepository userRepository;
    
    // 构造器注入 - 推荐
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Setter 注入:适合可选依赖,允许对象创建后动态修改依赖-23

java
复制
下载
@Service
public class UserService {
    private UserRepository userRepository;
    
    @Autowired  // 可选:也可以不加 @Autowired,Spring 4.3+ 自动推断
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

字段注入:写法最简洁,但框架侵入性强,Spring 官方不推荐用于生产环境-23

java
复制
下载
@Service
public class UserService {
    @Autowired  // 不推荐:与 Spring 框架强耦合,易出现 NPE
    private UserRepository userRepository;
}

四、IoC 与 DI 的关系与区别

两者关系一句话总结

IoC 是设计思想,DI 是实现手段;IoC 回答“谁来控制”,DI 回答“如何传递”。

详细对比

维度IoC(控制反转)DI(依赖注入)
本质设计原则、架构思想具体的设计模式、实现技术
范畴宽泛,涵盖程序流程控制具体,专注于对象依赖管理
回答的问题“谁来控制?”“怎么传递依赖?”
实现方式DI、服务定位器、模板方法等构造器注入、Setter 注入、字段注入

关键辨析

  • 一个系统可以存在 IoC 但不使用 DI,例如通过 JNDI 查找服务,控制权已交予容器,但未发生“注入”动作-2

  • 当使用依赖注入时,一定已经在应用 IoC 的原则,因为注入行为由外部容器驱动-1

  • 若仅在类内部调用另一个类的 Setter 方法并传入实例,该行为本身不是 DI,因为控制权仍在当前类,并非由外部容器驱动-2

五、代码示例:从混乱到优雅

传统方式(高耦合)

java
复制
下载
public class OrderService {
    private PaymentService payment = new AlipayService();  // 硬编码
    
    public void pay() {
        payment.process();  // 想换成微信支付?改代码重编译!
    }
}

问题OrderService 直接依赖 AlipayService 的具体实现,换支付方式必须改源码。

IoC + DI 方式(低耦合)

java
复制
下载
@Service
public class OrderService {
    private final PaymentService payment;  // 依赖接口,不依赖具体实现
    
    @Autowired  // 构造器注入(Spring 4.3+ 可省略此注解)
    public OrderService(PaymentService payment) {
        this.payment = payment;
    }
    
    public void pay() {
        payment.process();  // 具体是支付宝还是微信,由配置决定
    }
}

改进效果

  • OrderService 只依赖 PaymentService 接口,不关心具体实现;

  • 更换支付方式只需修改配置(如 @Primary@Qualifier),无需改动业务代码;

  • 单元测试时可轻松传入 Mock 对象。

六、底层原理:IoC 容器如何工作?

技术支撑

Spring IoC 的底层实现主要依赖两大技术:

  1. 反射(Reflection) :在运行时动态获取类的构造器、方法和属性,实现对象的实例化与依赖注入;

  2. 设计模式:工厂模式(Bean 的创建与获取)、模板方法模式(Bean 生命周期控制)等--11

IoC 容器核心流程

以最常用的注解配置为例,Spring 容器从启动到创建 Bean 的完整流程如下:

text
复制
下载
┌─────────────────────────────────────────────────────────────┐
│ Step 1: 容器初始化 → 扫描包路径,解析 @Component 注解            │
│            ↓                                                │
│ Step 2: 封装 BeanDefinition → 生成 Bean 的“说明书”            │
│            ↓                                                │
│ Step 3: 注册到 BeanDefinitionRegistry → 存入 Map              │
│            ↓                                                │
│ Step 4: 实例化 Bean → 通过反射调用构造器创建对象实例              │
│            ↓                                                │
│ Step 5: 依赖注入 → 为实例填充 @Autowired 标注的属性              │
│            ↓                                                │
│ Step 6: 执行初始化回调 → @PostConstruct、InitializingBean      │
│            ↓                                                │
│ Step 7: Bean 就绪 → 存入单例池(singletonObjects),供业务使用   │
└─────────────────────────────────────────────────────────────┘

BeanDefinition 是关键——它包含了 Bean 的所有信息:类名、作用域、依赖关系、初始化方法等,相当于 Bean 的“说明书”-11

核心接口:BeanFactory 与 ApplicationContext

  • BeanFactory:最基础的 IoC 容器接口,提供 getBean() 等核心能力,采用懒加载模式,调用时才创建 Bean;

  • ApplicationContext:日常开发使用的增强版容器,继承 BeanFactory,在启动时即创建所有单例 Bean,并额外支持国际化、事件发布等企业级功能-11

七、进阶话题:Spring 如何解决循环依赖?

什么是循环依赖?

循环依赖指两个或多个 Bean 之间互相持有对方的引用,形成闭环。最典型的场景:Bean A 依赖 Bean B,Bean B 又依赖 Bean A-36

java
复制
下载
@Component class A {
    @Autowired private B b;   // A 依赖 B
}
@Component class B {
    @Autowired private A a;   // B 依赖 A,形成循环
}

Spring 的解决方案:三级缓存

为了解决单例 Bean 的循环依赖问题,Spring 设计了 三级缓存机制,通过提前暴露半成品 Bean 的方式打破依赖闭环-36

缓存级别缓存名称作用
一级缓存singletonObjects存放完全初始化完成的成品 Bean,供业务直接使用
二级缓存earlySingletonObjects存放提前暴露的半成品 Bean(已实例化但未完成依赖注入)
三级缓存singletonFactories存放 ObjectFactory 工厂对象,用于生成早期引用

为什么需要三级而非二级? 因为 Spring 需要兼顾两个需求:保证循环依赖能解,同时保证 AOP 代理生效。如果只有二级缓存,就得在 Bean 创建实例后立刻决定是否生成代理,但此时尚未走到初始化步骤,无法判断是否需要增强。三级缓存将“是否生成代理”延迟到第一次被其他 Bean 引用时再决定,实现按需代理-37

注意:Spring 的三级缓存仅能解决单例 + Setter/字段注入场景下的循环依赖。构造器注入的循环依赖无法解决,会直接抛出异常。

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

面试题 1:什么是 Spring 的 IoC?有什么作用?

标准回答

IoC(Inversion of Control,控制反转)是一种设计思想,指的是将对象的创建、依赖关系的管理和生命周期的控制从程序本身转移给 Spring 容器。开发者只需要声明依赖关系,不需要手动创建对象。IoC 的主要作用是降低代码耦合度、提升可测试性和可维护性

踩分点:对象创建权移交、解耦、容器管理

面试题 2:IoC 和 DI 有什么区别?

标准回答

IoC 是一种设计思想,DI 是 IoC 的具体实现方式。IoC 回答的是“谁来控制”的问题,即控制权从程序移交给了容器;DI 回答的是“怎么传递”的问题,即通过构造器注入、Setter 注入或字段注入等方式将依赖传递给对象。没有 DI,IoC 无法落地;没有 IoC,DI 只是普通的方法传参

踩分点:思想 vs 实现、不同维度、不可互换

面试题 3:Spring 的依赖注入有哪几种方式?推荐哪种?

标准回答

Spring 提供三种主要的依赖注入方式:构造器注入、Setter 注入和字段注入构造器注入是官方最推荐的方式,因为它能保证依赖不可变、强制注入、便于单元测试。字段注入写法最简洁,但框架侵入性强、易出现 NPE,不推荐在生产环境中使用。

踩分点:三种方式对比、构造器注入的优点、字段注入的缺点

面试题 4:Spring 如何解决循环依赖问题?

标准回答

Spring 通过三级缓存机制来解决单例 Bean 的循环依赖问题。三级缓存分别是:一级缓存 singletonObjects(存成品 Bean)、二级缓存 earlySingletonObjects(存半成品 Bean)、三级缓存 singletonFactories(存 ObjectFactory 工厂)。核心原理是提前暴露正在创建中的 Bean——当一个 Bean 实例化后但还未完成依赖注入时,就将它提前放入三级缓存,这样其他 Bean 在依赖它时可以直接获取到引用,从而打破依赖闭环。三级缓存同时兼顾了循环依赖破局和 AOP 代理的正确生成。

踩分点:三级缓存的名称和作用、提前暴露、AOP 代理场景

九、结尾总结

核心知识点回顾

  • IoC 是设计思想:将对象创建和依赖管理的控制权从程序代码移交给容器;

  • DI 是实现手段:通过构造器注入、Setter 注入、字段注入实现 IoC;

  • 二者关系IoC 回答“谁来控制”,DI 回答“如何传递”,本质不同但紧密配合;

  • 底层原理:Spring 依赖反射 + 设计模式实现 IoC 容器;

  • 循环依赖:Spring 通过三级缓存解决单例 Bean 的循环依赖问题,核心是提前暴露

  • 注入方式推荐:构造器注入 > Setter 注入 > 字段注入。

重点提示

  • ⭐ 面试中常被问“IoC 和 DI 的关系”,记住核心公式:IoC = 思想,DI = 实现方式

  • ⭐ 字段注入方便但易出问题,构造器注入才是生产环境首选

  • ⭐ 三级缓存的名字和作用是高频考点,建议熟记。

下篇预告

本系列下一篇文章将深入解析 Spring 的另一核心机制——AOP(面向切面编程),从动态代理到底层实现,结合代码示例讲透 Spring 如何实现声明式事务管理,欢迎持续关注。