从 0 开始理解 Spring 的核心思想 —— IoC 和 DI(1)

前言

时光如白驹过隙,一晃笔者又一个月没更新了。

这段时间,一方面工作上有项目冲刺,另一方面我也在系统地梳理 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 生态的基石,我希望通过这个系列帮助大家真正理解它的设计思想,而不仅仅是停留在"会用注解"的层面。

如果你对这篇文章有任何想法或疑问,欢迎在评论区留言。我们一起交流,一起进步。

下期见!

相关推荐
莫寒清5 小时前
Apache Tika
java·人工智能·spring·apache·知识图谱
工业甲酰苯胺6 小时前
一文学习 Spring AOP 源码全过程
java·学习·spring
昱宸星光7 小时前
Xnio源码分析
java·jvm·spring
玄〤8 小时前
个人博客网站搭建day6--Spring Boot自定义RedisTemplate配置:优化序列化与Java8时间类型支持
java·spring boot·redis·后端·spring
知我Deja_Vu8 小时前
@Transactional 与 @Transactional(rollbackFor = Exception.class) 的区别详解
java·spring
三水不滴8 小时前
利用SpringCloud Gateway 重试 + 降级解决第三方接口频繁超时问题,提升性能
经验分享·笔记·后端·spring·spring cloud·gateway
葵续浅笑1 天前
从Spring拦截器到Filter过滤器:一次报文修改加解密的填坑经验
java·后端·spring
天若有情6731 天前
IoC不止Spring!求同vs存异,两种反向IoC的核心逻辑
java·c++·后端·算法·spring·架构·ioc