Spring 的 IoC 与 DI 原理:从“主动 new”到“被动注入”的核心进化

小编头像

小编

管理员

发布于:2026年04月28日

8 阅读 · 0 评论

发布时间:2026年4月8日 09:30(北京时间)

开篇引入

在 Java 企业级开发领域,Spring 框架几乎成为了“标准答案”。而支撑起整个 Spring 生态的两根支柱,正是 IoC(控制反转,Inversion of Control)DI(依赖注入,Dependency Injection)。无论是 Spring Boot 的自动配置,还是 Spring Cloud 的微服务治理,底层都离不开 IoC 容器的支撑——据统计,超过 80% 的 Spring 核心模块直接或间接依赖 IoC 容器提供的服务-1

很多开发者虽然每天都在写 @Autowired、用 Spring 管理 Bean,却对 IoC 和 DI 的概念边界一知半解——有人觉得它们是一回事,有人知道不一样但说不出区别。面试官最常问的就是:“IoC 和 DI 是什么关系?”,不少人被这道题卡住。

本文讲解范围: 从传统开发的痛点出发,系统讲解 IoC 的设计思想与 DI 的实现方式,理清二者的关系,并通过代码示例和底层原理分析,帮助读者建立完整的知识链路。本文为系列第一篇,后续将继续深入 Bean 生命周期、AOP 原理等内容。


痛点切入:为什么需要 IoC 和 DI?

在引入 Spring 之前,Java 开发中最常见的代码模式是这样的:

java
复制
下载
// 传统开发方式:紧耦合
public class OrderService {
    // 硬编码依赖——直接 new 具体实现类
    private PaymentService payment = new AlipayService();
    private Logger logger = new FileLogger("/tmp/log");
    
    public void pay() {
        payment.process();  // 想换成微信支付?改代码、重新编译!
        logger.log("支付完成");
    }
}

这种方式有什么问题?

  1. 高耦合OrderService 直接依赖 AlipayService 的具体实现,而不是依赖接口。一旦要替换支付方式(比如从支付宝切换到微信支付),就必须修改 OrderService 的源代码并重新编译。

  2. 扩展性差:新增一种支付方式时,需要改动所有用到支付服务的类。

  3. 难以测试:单元测试时无法方便地替换为 Mock 对象。

  4. 依赖蔓延:如果 AlipayService 自身还依赖其他类,开发者为了拿到一个对象,可能被迫创建一整条依赖链上的所有对象-8

为了解决这些问题,软件工程师们提出了一个核心设计思想:控制反转——把对象的创建和管理权力从应用程序代码中“反转”给外部容器。


核心概念讲解(一):IoC——控制反转

定义

IoC(Inversion of Control,控制反转) 是一种设计思想,其核心是:对象的创建与依赖关系的管理,不再由程序代码主动控制,而是交给外部容器来完成-2

关键词拆解

  • “控制” :指的是对成员变量赋值的控制权,即“谁来创建对象”“谁来管理依赖关系”。

  • “反转” :相对于传统开发的“正转”——传统模式下,开发者主动 new 对象、主动获取依赖;反转模式下,由容器来帮忙创建及注入依赖对象-

生活化类比

假设你要组织一次家庭聚餐:

  • 传统方式:你要亲自去超市采购食材(new 对象)、洗菜切菜(处理依赖)、炒菜装盘(组装对象)——所有事情自己包揽。

  • IoC 方式:你直接预订上门厨师服务。你只需要告诉厨师“周末中午 10 人聚餐,要 3 个热菜、2 个凉菜”(声明需求),厨师会自动完成食材采购、备菜、烹饪的全流程,最终把做好的菜直接端上桌——你完全不用操心食材怎么来、依赖怎么配-51

IoC 的核心价值: 不是“少写几行 new 代码”,而是实现了彻底的解耦——对象和对象的创建逻辑解耦,组件和组件的依赖关系解耦。


关联概念讲解(二):DI——依赖注入

定义

DI(Dependency Injection,依赖注入) 是一种设计模式,是 IoC 思想的具体实现方式。它指的是:容器在运行时,动态地将某个依赖对象注入到组件中-8

它与 IoC 的关系

很多人容易混淆这两个概念,它们的关系其实非常清晰:

维度IoC(控制反转)DI(依赖注入)
定位设计思想(宏观)实现方式(微观)
关注点“谁来管”——控制权的转移“怎么管”——依赖的传递
描述角度从容器的角度:容器控制应用程序从应用程序的角度:依赖容器注入所需资源
一句话概括把对象的创建和管理交给容器容器把依赖的对象“送”进来-27

一句话记忆法:IoC 是“指导思想”,DI 是“落地动作”。

依赖注入的三种方式

Spring 提供了三种主要的依赖注入方式-47

1. 构造器注入(推荐 ⭐⭐⭐⭐⭐)

java
复制
下载
@Service
public class UserService {
    private final UserRepository repository;  // final 确保不可变

    // 只有一个构造器时,@Autowired 可省略
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
}

优点:依赖不可变、对象创建时即完成完整初始化、便于单元测试(直接 new 传入 Mock 对象)。

2. Setter 注入

java
复制
下载
@Service
public class OrderService {
    private PaymentService paymentService;

    @Autowired
    public void setPaymentService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

优点:灵活、支持可选依赖;缺点:依赖可被外部修改为 null,存在安全隐患。

3. 字段注入(最常用但需谨慎)

java
复制
下载
@Service
public class ProductService {
    @Autowired  // 最简洁的写法
    private CategoryService categoryService;
}

优点:代码最少、最爽;缺点:容易乱用、依赖关系不明确、可被反射修改值。

💡 实战建议:大厂普遍采用构造器注入,原因包括:依赖不可变、避免空指针、便于测试、不支持循环依赖时能提前报错-


概念关系总结

用一句话串起全文核心逻辑:

IoC 是一种“把控制权交给容器”的设计思想,而 DI 是实现这种思想的“具体手段”——通过构造器、Setter 或字段注入,容器把依赖的对象“送”进你的组件里。

记忆口诀:IoC 是“你只管说要什么,不用管怎么来”;DI 是“容器负责把你需要的送过来”。


代码示例:从“手动 new”到“Spring 注入”

传统方式(紧耦合)

java
复制
下载
// Dao 层
public class UserDaoImpl {
    public void save() {
        System.out.println("保存用户");
    }
}

// Service 层——硬编码依赖
public class UserService {
    private UserDaoImpl userDao = new UserDaoImpl();  // 直接 new
    
    public void createUser() {
        userDao.save();
    }
}

Spring 方式(解耦)

1. 配置类(JavaConfig)

java
复制
下载
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    // 容器会自动扫描并管理 Bean
}

2. 标注组件

java
复制
下载
@Repository  // 声明为 Bean
public class UserDaoImpl {
    public void save() {
        System.out.println("保存用户");
    }
}

@Service
public class UserService {
    private final UserDaoImpl userDao;  // 声明依赖

    // 构造器注入——Spring 会自动传入实例
    public UserService(UserDaoImpl userDao) {
        this.userDao = userDao;
    }

    public void createUser() {
        userDao.save();
    }
}

3. 启动容器

java
复制
下载
public class Main {
    public static void main(String[] args) {
        ApplicationContext context = 
            new AnnotationConfigApplicationContext(AppConfig.class);
        UserService userService = context.getBean(UserService.class);
        userService.createUser();
    }
}

执行流程解析

  1. 容器启动时扫描 @ComponentScan 指定的包路径;

  2. 找到 @Service@Repository 等注解标注的类,生成 BeanDefinition 元数据;

  3. 根据构造器参数类型,递归查找并创建依赖的 Bean 实例;

  4. 通过反射调用构造器,完成对象实例化和依赖注入;

  5. 将完全装配好的 Bean 存入容器,供应用程序获取使用。


底层原理:反射——Spring 的灵魂引擎

IoC 容器之所以能够“动态”创建对象和注入依赖,底层依赖的核心技术就是 Java 反射机制

反射在 Spring 中的三个关键应用

1. 创建 Bean 实例

容器根据类全限定名(如 "com.example.UserService"),通过类加载器获取 Class 对象,再调用 clz.newInstance() 或带参数的构造器反射方法创建实例-35

java
复制
下载
// 简化版实现
Class<?> clz = Class.forName("com.example.UserService");
Object instance = clz.getDeclaredConstructor().newInstance();

2. 构造器注入

容器获取目标类的所有构造器信息(Constructor 对象),筛选出匹配依赖参数的构造器,通过 constructor.newInstance(args) 创建实例并同时完成依赖注入-35

3. Setter/字段注入

容器通过 getDeclaredMethods() 获取所有方法,找到以 set 开头的 setter 方法,通过 method.invoke(bean, propertyBean) 注入依赖;对于 @Autowired 字段注入,则通过 field.setAccessible(true) 突破 private 访问限制,再调用 field.set(bean, value) 赋值-35

为什么用反射?

  • 动态性:直到运行时才知道要创建哪个类的实例(配置可以是 XML 或注解,在代码编译时不可知);

  • 解耦:代码中不需要硬编码 new 语句,依赖关系完全由配置决定;

  • 灵活性:支持 private 字段注入、构造器参数自动匹配等高级特性。

📌 性能提示:反射调用确实比直接调用慢(大约慢 1~2 个数量级),但对于大多数企业应用,容器启动时的一次性反射开销完全可以接受-


高频面试题与参考答案

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

参考答案

  • IoC(Inversion of Control,控制反转) 是一种设计思想,将对象的创建和依赖管理的控制权从应用程序代码转移到外部容器。

  • DI(Dependency Injection,依赖注入) 是 IoC 思想的具体实现方式,指容器在运行时动态地将依赖对象注入到组件中。

  • 关系:IoC 是“指导思想”,DI 是“落地动作”。IoC 回答“谁来管”的问题,DI 回答“怎么给”的问题-50-51

💡 得分要点:先分别给出定义,再点明“思想 vs 实现”的核心关系,最后可补充 IoC 还有依赖查找(DL)等其他实现方式。

Q2:Spring 中依赖注入有哪几种方式?分别有什么优缺点?

参考答案

注入方式优点缺点推荐度
构造器注入依赖不可变、对象完整、便于测试参数多时构造器较长⭐⭐⭐⭐⭐
Setter 注入灵活、支持可选依赖依赖可被外部修改为 null⭐⭐
字段注入(@Autowired)代码最简洁依赖关系不明确、可被反射修改⭐⭐⭐

大厂推荐:构造器注入-47

Q3:Spring IoC 容器的底层用到了哪些技术?简要说明其工作原理。

参考答案

主要依赖 工厂模式 + 反射机制

  1. 容器启动时解析配置(XML/注解),生成 BeanDefinition 元数据;

  2. 通过反射获取类的构造器信息,创建 Bean 实例;

  3. 通过反射调用 setter 方法或直接给字段赋值,完成依赖注入;

  4. 将完全装配的 Bean 存入容器(如 DefaultListableBeanFactory 使用 ConcurrentHashMap 存储)-6-1

Q4:BeanFactory 和 ApplicationContext 有什么区别?

参考答案

对比维度BeanFactoryApplicationContext
加载时机懒加载(第一次 getBean 时才初始化)预加载(启动时初始化所有 singleton Bean)
功能范围仅提供基本的 getBean、依赖注入扩展了 AOP、事件发布、国际化、资源加载等企业级特性
使用场景嵌入式系统、资源受限环境绝大多数企业项目(推荐使用)

ApplicationContextBeanFactory 的子接口,包含了 BeanFactory 的全部功能-2-1

Q5:Spring 为什么推荐使用构造器注入?

参考答案(四个要点):

  1. 依赖不可变:依赖对象可以用 final 修饰,保证线程安全;

  2. 避免空指针:对象创建时依赖就已就位,不会出现“半初始化”状态;

  3. 便于单元测试:可以直接 new 对象并传入 Mock 依赖,无需启动容器;

  4. 循环依赖检测:构造器注入的循环依赖会在启动时报错,便于尽早发现问题--47


结尾总结

核心知识点回顾

  1. IoC(控制反转) :一种设计思想,将对象的创建和管理权交给外部容器——回答“谁来管”。

  2. DI(依赖注入) :IoC 的具体实现方式,容器通过构造器、Setter 或字段注入依赖——回答“怎么给”。

  3. 二者关系思想与实现——IoC 是目标,DI 是手段。

  4. 底层支撑反射机制是 Spring 实现动态创建和注入的技术基石。

  5. 最佳实践:日常开发优先使用构造器注入,保证依赖的不可变性和代码的可测试性。

重点易错点提醒

  • 不要混淆 IoC 和 DI:它们不是一回事,IoC 是思想,DI 是实现。

  • 不要过度使用字段注入:虽然代码最简洁,但会隐藏依赖关系,不利于测试和维护。

  • 不要忽略反射的性能代价:虽然对企业应用影响不大,但理解这一点有助于深入掌握底层原理。

下篇预告

本文讲解了 IoC 和 DI 的核心概念与反射原理。下一篇我们将深入 Bean 的生命周期,从实例化到销毁的完整流程,以及 BeanPostProcessor 扩展点如何实现 AOP 和事务管理等高级功能,敬请期待!


📌 本文内容基于 Spring Framework 5.x/6.x 核心原理整理,如有疑问欢迎留言交流。

标签:

相关阅读