前言
时光如白驹过隙,一晃笔者又一个月没更新了。
这段时间,一方面工作上有项目冲刺,另一方面我也在系统地梳理 Spring 的学习笔记。Spring 作为 Java 生态的基石框架,我希望能写出真正帮助大家"理解思想"的文章,而不仅仅是"教用法"。
今天,我们正式开启 Spring 系列博客的旅程。
本篇聚焦 Spring 的核心思想之一 ------ IoC(控制反转)与 DI(依赖注入)。我们将通过问题驱动的形式,从"没有 Spring 的世界"开始,逐步揭开 Spring 的神秘面纱。
第一部分:一个没有 Spring 的世界
假设我们在开发一个电商系统,需要实现订单处理功能。让我们看看没有 Spring 的代码长什么样。
第一版:最简单的实现
java
// 用户服务
public class UserService {
public User getUserById(Long id) {
System.out.println("查询数据库获取用户信息...");
return new User(id, "张三");
}
}
// 订单服务
public class OrderService {
private UserService userService;
public OrderService() {
// 在构造函数中创建依赖对象
this.userService = new UserService();
}
public void processOrder(Long userId) {
User user = userService.getUserById(userId);
System.out.println("为用户 " + user.getName() + " 处理订单");
}
}
// 客户端调用
public class Application {
public static void main(String[] args) {
OrderService orderService = new OrderService();
orderService.processOrder(1L);
}
}
这段代码看起来很简单,对吧?但问题很快就会浮现。
第二版:需求变更带来的连锁反应
现在,UserService 需要连接数据库,构造函数需要传入数据库连接:
java
// UserService 需要数据库连接
public class UserService {
private DatabaseConnection connection;
public UserService(DatabaseConnection connection) {
this.connection = connection;
}
// ...
}
// 所有依赖 UserService 的地方都要修改!
public class OrderService {
private UserService userService;
public OrderService() {
// 这里也必须修改!
this.userService = new UserService(new DatabaseConnection());
}
// ...
}
一个需求的变更,导致了 OrderService 也必须修改------这就是耦合带来的维护成本。
第三版:当依赖越来越多
随着业务发展,OrderService 还需要依赖更多服务:
java
public class OrderService {
private UserService userService;
private ProductService productService;
private InventoryService inventoryService;
private PaymentService paymentService;
private NotificationService notificationService;
public OrderService() {
// 构造函数变得越来越臃肿
this.userService = new UserService(new DatabaseConnection());
this.productService = new ProductService(new DatabaseConnection(), new CacheService());
this.inventoryService = new InventoryService(new HttpClient());
this.paymentService = new PaymentService(new PaymentGateway());
this.notificationService = new NotificationService(new EmailSender(), new SmsSender());
// 这里的 new 越来越多,耦合越来越重
}
public void processOrder(Long userId, Long productId) {
// 业务逻辑...
}
}
看到问题了吗?OrderService 不仅要处理自己的业务逻辑,还要负责创建所有依赖的对象。更糟糕的是,它需要知道这些依赖的依赖------比如 ProductService 需要 DatabaseConnection 和 CacheService。
第二部分:问题诊断------传统开发的三大痛点
通过上面的代码演变,我们可以总结出传统开发模式的三大核心问题:
痛点一:高耦合
java
public class OrderService {
private UserService userService;
public OrderService() {
// OrderService 与 UserService 的具体实现紧密绑定
this.userService = new UserService();
// 如果想换成 UserServiceImpl2,必须修改代码
}
}
痛点二:难以测试
java
public class OrderServiceTest {
@Test
public void testProcessOrder() {
OrderService orderService = new OrderService();
// 无法替换成 MockUserService!
// 每次测试都会真实调用数据库,测试变得缓慢且不可控
}
}
痛点三:职责过重
java
// OrderService 违反了"单一职责原则"
public class OrderService {
// 1. 负责创建自己的依赖
// 2. 负责管理依赖的生命周期
// 3. 负责自己的业务逻辑
// 它不应该关心:
// - DatabaseConnection 如何创建
// - CacheService 如何配置
// - HttpClient 如何初始化
}
核心问题 :对象既要负责业务逻辑,又要负责依赖管理------职责过多。
第三部分:控制反转(IoC)思想的诞生
面对这些痛点,聪明的程序员们开始思考一个哲学问题:
对象是否应该自己负责创建它所需要的东西?
就像你去餐厅吃饭------你是希望自己买菜、洗菜、炒菜,还是只负责点菜,让厨师去操心这些?显然,后者更合理。
这就是 控制反转(IoC,Inversion of Control) 的核心思想:
将对象的创建和管理权从对象内部转移到外部容器
让我们用代码感受这种转变:
重构前(传统模式)
java
public class OrderService {
private UserService userService;
public OrderService() {
// 控制权在自己手中:自己创建
this.userService = new UserService();
}
}
重构后(IoC 模式)
java
public class OrderService {
private UserService userService;
public OrderService(UserService userService) {
// 控制权被"反转":别人创建好传给我
this.userService = userService;
}
}
看到区别了吗?
- 传统模式 :OrderService 主动创建依赖(控制在自己手中)
- IoC 模式 :OrderService 被动接收依赖(控制权交给外部)
这正是著名的好莱坞原则 :Don't call us, we'll call you.(别找我,我会来找你)
IoC 前后对比图
传统模式:
Application → new OrderService() → new UserService() → new DatabaseConnection()
IoC 模式:
Application → IoC容器(创建所有对象) → 注入给 OrderService
↓
UserService ← DatabaseConnection
第四部分:依赖注入(DI)的三种实现方式
那么,IoC 容器是如何把依赖"送"给对象的呢?这就是 DI(Dependency Injection,依赖注入) 要做的事。
依赖注入有三种主流实现方式:
方式一:构造函数注入(最推荐)
java
@Component
public class OrderService {
private final UserService userService; // final 确保不可变性
private final ProductService productService;
// 通过构造函数注入
@Autowired // Spring 会自动找到匹配的 Bean 传入
public OrderService(UserService userService, ProductService productService) {
this.userService = userService;
this.productService = productService;
}
}
优点:
- 支持 final 修饰,保证不可变性
- 依赖明确,一眼就能看出需要什么
- 便于单元测试(直接 new 并传入 Mock 对象)
- 避免循环依赖问题
方式二:Setter 方法注入
java
@Component
public class OrderService {
private UserService userService;
@Autowired // Spring 会调用这个方法注入依赖
public void setUserService(UserService userService) {
this.userService = userService;
}
}
适用场景: 可选依赖、需要动态更改的依赖
方式三:字段注入(最简洁,但有隐患)
java
@Component
public class OrderService {
@Autowired // 直接注入字段
private UserService userService;
}
缺点:
- 无法声明为 final
- 单元测试时需要反射才能注入
- 隐藏了依赖关系
最佳实践 :优先使用构造函数注入,它让依赖关系最清晰。
第五部分:Spring 容器是如何工作的?
理解了 IoC 和 DI 的概念,我们来看看 Spring 具体是如何实现的。
Spring 容器的核心模型
java
// 1. 定义 Bean(告诉 Spring 要管理哪些类)
@Component
public class UserService {
public User getUserById(Long id) {
return new User(id, "张三");
}
}
@Component
public class OrderService {
private final UserService userService;
@Autowired // 告诉 Spring 需要注入 UserService
public OrderService(UserService userService) {
this.userService = userService;
}
public void processOrder(Long userId) {
User user = userService.getUserById(userId);
System.out.println("处理订单:" + user.getName());
}
}
// 2. 启动容器(让 Spring 开始工作)
@Configuration
@ComponentScan("com.example")
public class AppConfig {
public static void main(String[] args) {
// 创建 Spring 容器
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
// 直接从容器获取 OrderService(已经自动注入好 UserService)
OrderService orderService = context.getBean(OrderService.class);
orderService.processOrder(1L);
}
}
Spring 容器内部的工作流程
1. 启动阶段:
┌─────────────────────────────────────┐
│ Spring 容器扫描指定包 │
│ ↓ │
│ 发现 @Component 类: │
│ - UserService │
│ - OrderService │
│ ↓ │
│ 创建 BeanDefinition(类元数据) │
└─────────────────────────────────────┘
2. 实例化阶段:
┌─────────────────────────────────────┐
│ 根据 BeanDefinition 创建对象实例 │
│ ↓ │
│ UserService userService = new UserService()│
│ OrderService orderService = new OrderService()│
└─────────────────────────────────────┘
3. 注入阶段:
┌─────────────────────────────────────┐
│ 分析依赖关系: │
│ OrderService 需要 UserService │
│ ↓ │
│ 从容器中找到 UserService 实例 │
│ ↓ │
│ 通过构造函数注入给 OrderService │
└─────────────────────────────────────┘
4. 就绪阶段:
┌─────────────────────────────────────┐
│ 所有 Bean 准备就绪,等待使用 │
│ orderService.processOrder() 可直接调用 │
└─────────────────────────────────────┘
第六部分:总结与下期预告
本节核心要点
| 概念 | 通俗理解 | 技术定义 |
|---|---|---|
| 耦合问题 | 改一处代码,到处都要改 | 类与类之间的直接依赖 |
| IoC(控制反转) | 把"造东西"的活交给别人 | 将对象创建权交给容器 |
| DI(依赖注入) | 别人把造好的东西给我 | 容器负责建立依赖关系 |
| Spring 容器 | 一个超级工厂 | 管理 Bean 生命周期和依赖的框架 |
一个小思考题
如果我们有多个同类型的 Bean(比如多个 UserService 的实现类),Spring 该如何决定注入哪一个?
这个问题我们将在下一篇文章中解答。
下期预告
《深入理解 Spring IoC 容器------Bean 的生命周期与作用域》
我们将深入源码,看看 Spring 容器内部是如何管理 Bean 的创建、初始化、销毁的全过程。
写在最后
博客停更的这一个月,我一直在思考如何把复杂的技术讲得清晰。Spring 作为 Java 生态的基石,我希望通过这个系列帮助大家真正理解它的设计思想,而不仅仅是停留在"会用注解"的层面。
如果你对这篇文章有任何想法或疑问,欢迎在评论区留言。我们一起交流,一起进步。
下期见!