IoC 控制反转:从概念到 Spring 的实现

文章来源声明: 原文作者:步行cgn; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684080599313678370; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

从概念、实现到误区层层铺开,厘清了 IoC 与 DI 的关系,适合刚接触 Spring 或想补牢基础、复习面试要点的开发者。

IoC 控制反转:从概念到 Spring 的实现 ------------------------

一、什么是控制反转?

控制反转(Inversion of Control,IoC)是一种设计原则,它的核心思想是:将对象的创建、依赖管理和生命周期控制的权力,从应用程序代码中转移到外部容器

传统编程中,对象自己负责创建它依赖的对象,就像一个人要自己买菜、做饭、洗碗。控制反转之后,你只需要告诉容器“我需要什么”,容器会把准备好的对象送到你手上,你不需要关心它怎么来的、什么时候销毁。

从技术角度说,IoC 反转的是获取依赖对象的控制权。以前是代码主动 new 一个依赖,现在是被动等待容器注入。

二、为什么需要控制反转?

在没有 IoC 的时代,代码是这样的:

<span>public</span> <span>class</span> <span>UserService</span> {
    <span>private</span> <span>UserDao</span> <span>userDao</span> <span>=</span> <span>new</span> <span>UserDaoImpl</span>();
    <span>private</span> <span>EmailService</span> <span>emailService</span> <span>=</span> <span>new</span> <span>EmailService</span>();
    
    <span>public</span> <span>void</span> <span>register</span><span>(User user)</span> {
        userDao.save(user);
        emailService.sendWelcomeEmail(user.getEmail());
    }
}

问题很明显:UserService 直接依赖 UserDaoImplEmailService 这两个具体类。如果要把 UserDaoImpl 换成 UserDaoRedis,必须修改 UserService 的源码。单元测试时也没法把 UserDao 替换成 Mock 对象。

IoC 解决的就是这个问题:让 UserService 只依赖接口,具体实现由容器注入。

<span>@Service</span>
<span>public</span> <span>class</span> <span>UserService</span> {
    <span>@Autowired</span>
    <span>private</span> UserDao userDao;
    
    <span>@Autowired</span>
    <span>private</span> EmailService emailService;
    
    <span>public</span> <span>void</span> <span>register</span><span>(User user)</span> {
        userDao.save(user);
        emailService.sendWelcomeEmail(user.getEmail());
    }
}

此时 UserService 不再关心 UserDao 的具体实现是什么,也不关心 EmailService 怎么创建。它只声明“我需要这两个东西”,容器负责提供。

三、控制反转的实现方式:依赖注入

IoC 是设计思想,依赖注入(Dependency Injection,DI)是实现这个思想的具体手段。Spring 通过 DI 实现了 IoC。

依赖注入有三种常见方式:

构造器注入

<span>@Service</span>
<span>public</span> <span>class</span> <span>UserService</span> {
    <span>private</span> <span>final</span> UserDao userDao;
    
    <span>public</span> <span>UserService</span><span>(UserDao userDao)</span> {
        <span>this</span>.userDao = userDao;
    }
}

Spring 在创建 UserService 时,会从容器中找到 UserDao 类型的 Bean,作为参数传入构造方法。这种方式的优点是依赖不可变(final),且对象创建时依赖就已经就绪。

Setter 注入

<span>@Service</span>
<span>public</span> <span>class</span> <span>UserService</span> {
    <span>private</span> UserDao userDao;
    
    <span>@Autowired</span>
    <span>public</span> <span>void</span> <span>setUserDao</span><span>(UserDao userDao)</span> {
        <span>this</span>.userDao = userDao;
    }
}

Spring 先通过无参构造方法创建对象,再调用 Setter 方法注入依赖。

字段注入

<span>@Service</span>
<span>public</span> <span>class</span> <span>UserService</span> {
    <span>@Autowired</span>
    <span>private</span> UserDao userDao;
}

Spring 通过反射直接将依赖赋值给字段。这种方式代码最简洁,但不利于单元测试,也不支持 final 字段。

Spring 官方推荐构造器注入,因为它能保证依赖不为空,且更容易测试。

四、IoC 容器

IoC 容器是 Spring 实现控制反转的核心载体。容器负责:

  • 实例化 Bean:通过反射调用构造方法创建对象
  • 注入依赖:根据配置或注解,把依赖对象注入到目标对象中
  • 管理生命周期:控制 Bean 的初始化、使用和销毁
  • 管理作用域:支持单例、原型、请求、会话等作用域

在 Spring 中,容器的具体实现是 ApplicationContext。它启动时会扫描配置,生成 Bean 定义,然后依次实例化、注入、初始化所有非懒加载的单例 Bean。

<span>ApplicationContext</span> <span>context</span> <span>=</span> <span>new</span> <span>AnnotationConfigApplicationContext</span>(AppConfig.class);
<span>UserService</span> <span>userService</span> <span>=</span> context.getBean(UserService.class);

五、控制反转的“反转”体现在哪里?

传统方式中,控制流是:UserService 主动创建 UserDao,控制权在 UserService 手里。

IoC 之后,控制流是:容器创建 UserDao,然后把它注入给 UserServiceUserService 被动接收依赖,控制权转移到了容器。

这就是“反转”的含义:获取依赖的控制权从应用程序代码反转到了外部容器

六、IoC 在 Spring 中的实际体现

Spring 通过几个核心机制实现 IoC:

Bean 定义:通过 @Component@Service@Bean 等注解或 XML 配置,告诉容器需要管理哪些对象。

依赖注入:通过 @Autowired@Resource、构造器参数等,告诉容器对象之间的依赖关系。

自动装配:容器根据类型或名称自动匹配依赖,不需要手动指定。

条件装配:通过 @ConditionalOnClass@ConditionalOnMissingBean 等,根据条件决定是否创建某个 Bean。

作用域管理:默认单例,可以通过 @Scope 改成原型或其他作用域。

七、IoC 与 DI 的关系

很多人把 IoC 和 DI 混为一谈,但它们有明确区别:

概念含义
IoC设计原则,描述“控制权从代码转移到容器”
DI实现模式,描述“容器如何把依赖传递给对象”

IoC 是目标,DI 是手段。Spring 通过 DI 实现了 IoC。

八、IoC 带来的好处

降低耦合:对象只依赖接口,不依赖具体实现,替换实现无需修改调用方。

便于测试:可以注入 Mock 对象,单元测试不需要启动容器。

统一管理:容器集中管理所有 Bean,生命周期、作用域、依赖关系一目了然。

提高可扩展性:新增实现只需添加新类,不需要修改现有代码。

支持面向接口编程:代码天然地面向接口,而不是面向实现。

九、常见误区

误区一:IoC 就是依赖注入

IoC 是设计思想,DI 是实现方式。Spring 用 DI 实现了 IoC,但 IoC 不只有 DI 一种实现方式(依赖查找也是实现 IoC 的一种方式,但 Spring 主要使用 DI)。

误区二:用了 Spring 就自动实现了 IoC

只有把对象的创建和依赖管理交给 Spring 容器,才算实现了 IoC。如果代码里到处是 new,即使引入了 Spring,也没有真正用到 IoC。

误区三:字段注入是最好的方式

字段注入写起来最方便,但构造器注入更符合 IoC 的理念——依赖在对象创建时就确定,对象一旦创建就处于就绪状态。Spring 官方推荐构造器注入。

十、总结

控制反转的本质是把对象创建和依赖管理的控制权交给容器。它让代码从“主动创建依赖”变成“被动接收依赖”,从而实现模块间的松耦合。

在 Spring 中,IoC 通过依赖注入实现。容器负责实例化 Bean、注入依赖、管理生命周期。开发者只需要通过注解或配置声明依赖关系,剩下的交给容器。

理解 IoC,就理解了 Spring 最底层的设计哲学:框架控制流程,开发者专注业务