返回首页技术

Spring Boot IOC 原理:通俗理解控制反转

2026年03月24日7 min read

Spring Boot IOC 原理:通俗理解控制反转

前言

学 Java 的时候,IOC 和 DI 这两个词肯定没少听说。但刚入门的时候,看官方解释总觉得云里雾里的。今天我们就用大白话,把 IOC 讲清楚。

传统写法的问题

先来看看没用 IOC 之前,我们是怎么写代码的。

假设我们要做一个外卖系统,需要 OrderService(订单服务):

public class OrderService {
    // 直接在代码里 new 出来
    private PaymentService paymentService = new PaymentService();
    private NotificationService notificationService = new NotificationService();

    public void createOrder() {
        // 下单逻辑
        paymentService.pay();
        notificationService.notify();
    }
}

看起来没问题?但实际开发中会有这些麻烦:

  1. 写单元测试很痛苦 - 想单独测试 OrderService,但 PaymentService 和 NotificationService 也跟着被 new 出来了
  2. 换实现很麻烦 - 如果要把 PaymentService 换成 AliPayService,需要改源代码
  3. 耦合太紧密 - OrderService 和具体的实现类绑死了

什么是 IOC?

IOC 的全称是 Inversion of Control(控制反转),光看名字还是懵的,我们用生活中的例子来理解。

生活中的 IOC

想象你去餐厅吃饭:

没有 IOC 的方式:你需要自己带厨师、自己带锅、自己带食材...太累了。

有 IOC 的方式:你只管点菜,后厨(餐厅)帮你准备好一切。你不需要关心菜是谁做的、怎么做的,只需要说"我要这个"就行。

在程序里,IOC 容器就是那个"餐厅后厨"。你只需要声明"我需要一个 PaymentService",容器就会帮你准备好,你直接用就行。

public class OrderService {
    // 我需要一个 PaymentService,具体实现不用我管
    @Autowired
    private PaymentService paymentService;
}

什么是 DI?

DI 的全称是 Dependency Injection(依赖注入),它是实现 IOC 的手段。

其实就是把"依赖"(需要的对象)"注入"进来。怎么理解?

还是点菜的比喻:

  • 构造函数注入 - 相当于你打电话订餐,餐厅把菜送到你手上
  • Setter 注入 - 相当于你留了个地址,餐厅随时可以送餐过来
  • 字段注入 - 相当于你直接去柜台领餐(最简单但不推荐)
// 构造函数注入(推荐)
public class OrderService {
    private PaymentService paymentService;

    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

// Setter 注入
public class OrderService {
    private PaymentService paymentService;

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

Spring IOC 容器是怎么工作的?

看这张图:

┌─────────────────────────────────────────────────────┐
│                  Spring IOC 容器                    │
│  ┌─────────────────────────────────────────────┐   │
│  │     Bean 管理池(容器帮你管理的对象)         │   │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────────┐   │   │
│  │  │ User    │ │ Order   │ │ Payment     │   │   │
│  │  │ Service │ │ Service │ │ Service     │   │   │
│  │  └─────────┘ └─────────┘ └─────────────┘   │   │
│  └─────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────────┐
│                    你的代码                           │
│                                                     │
│  @Autowired                                         │
│  private UserService userService;  // 直接用!      │
│                                                     │
└─────────────────────────────────────────────────────┘

容器做了什么?

  1. 扫描 - 启动时扫描所有 @Component@Service@Repository@Controller
  2. 创建 - 根据类定义创建对象实例
  3. 装配 - 把依赖关系处理好(谁依赖谁、谁引用谁)
  4. 管理 - 对象的创建、初始化、销毁都归容器管

用代码说话

定义一个 Bean

// Service 层
@Service  // 本质上也是 @Component
public class UserService {
    public String getUserName(Long id) {
        return "用户" + id;
    }
}

// Controller 层
@Controller
public class UserController {
    @Autowired  // 注入
    private UserService userService;

    public void showUser() {
        String name = userService.getUserName(1L);
        System.out.println(name);
    }
}

启动类

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        // 这一行启动,IOC 容器就开始工作了
        SpringApplication.run(MyApplication.class, args);
    }
}

启动的时候,Spring 会自动扫描 UserServiceUserController,创建它们的实例,然后自动把 UserService 注入到 UserController 里。

Bean 的作用域

Spring 管理的对象叫 Bean,不同的 Bean 有不同的作用域:

作用域说明例子
singleton整个应用只有一个实例(默认)数据库连接池
prototype每次使用都创建新实例原型对象
request每次 HTTP 请求一个实例Web 请求对象
session每次 HTTP Session 一个实例用户会话对象
@Service
@Scope("prototype")  // 每次注入都创建新对象
public class UserService {
    // ...
}

懒加载 vs 即时加载

// 默认是即时加载 - 应用启动就创建
@Service
public class UserService { }

// 懒加载 - 第一次使用时才创建
@Service
@Lazy
public class UserService { }

总结

用一句话概括 IOC:

"你只管点菜,不用关心菜是谁做的、怎么做。Spring 容器就是那个帮你搞定一切的后厨。"

核心优势

  1. 松耦合 - 类之间不直接依赖,依赖的是接口
  2. 易测试 - 可以轻松替换成 mock 对象
  3. 易扩展 - 换实现只需要改配置,不用改代码
  4. 单例管理 - 容器统一管理 Bean 的生命周期

下期预告

下一篇文章我们聊聊 AOP(面向切面编程),看看"切面"到底是什么,以及如何在 Spring 中使用它。

如果觉得有帮助,欢迎在评论区留言讨论!

评论区