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();
}
}
看起来没问题?但实际开发中会有这些麻烦:
- 写单元测试很痛苦 - 想单独测试 OrderService,但 PaymentService 和 NotificationService 也跟着被 new 出来了
- 换实现很麻烦 - 如果要把 PaymentService 换成 AliPayService,需要改源代码
- 耦合太紧密 - 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; // 直接用! │
│ │
└─────────────────────────────────────────────────────┘
容器做了什么?
- 扫描 - 启动时扫描所有
@Component、@Service、@Repository、@Controller - 创建 - 根据类定义创建对象实例
- 装配 - 把依赖关系处理好(谁依赖谁、谁引用谁)
- 管理 - 对象的创建、初始化、销毁都归容器管
用代码说话
定义一个 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 会自动扫描 UserService 和 UserController,创建它们的实例,然后自动把 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 容器就是那个帮你搞定一切的后厨。"
核心优势
- 松耦合 - 类之间不直接依赖,依赖的是接口
- 易测试 - 可以轻松替换成 mock 对象
- 易扩展 - 换实现只需要改配置,不用改代码
- 单例管理 - 容器统一管理 Bean 的生命周期
下期预告
下一篇文章我们聊聊 AOP(面向切面编程),看看"切面"到底是什么,以及如何在 Spring 中使用它。
如果觉得有帮助,欢迎在评论区留言讨论!