Skip to content

Spring:IOC 与依赖注入 ​

S3-006必学进阶约 90 分钟JDK 21企业开发基础 · Spring 核心与 MVC

1. 本节定位 ​

到目前为止写的代码,依赖都是自己 new 出来的:

java
private final OrderRepository repository = new InMemoryOrderRepository();

本节要解决的就是这一行的两个问题:换实现要改源码、测试时要改源码。

Spring 的答案是把"创建与装配对象"这件事交给容器——这就是 IOC(控制反转)。本节用纯 Spring(不上 Boot)把容器的行为看清楚。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说清 IOC 与 DI 的关系(一个是思想、一个是手段);
  • 用 @Configuration + @Bean 装配对象,并解释"为什么换实现不用改业务类";
  • 说出三种注入方式的取舍,并知道为什么推荐构造器注入;
  • 说出单例与 prototype 作用域的区别;
  • 看懂循环依赖的报错,并知道为什么构造器注入会直接失败;
  • 说出容器启动时"Bean 是什么时候创建的"。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

看一段"典型的新手代码":

java
public class OrderService {
    private final OrderRepository repository = new JdbcOrderRepository("jdbc:mysql://...");
    private final Notifier notifier = new SmsNotifier("key-123");
}

三个问题:

问题后果
换实现要改源码测试时想换成内存实现?改代码。上线换短信服务商?改代码。
无法在测试里替换单元测试被迫连真实数据库(T1-010 测试替身)
依赖关系被隐藏看构造函数不知道这个类需要什么——依赖散落在字段初始化里

改成注入后的样子:

java
public class OrderService {
    private final OrderRepository repository;
    private final Notifier notifier;

    public OrderService(OrderRepository repository, Notifier notifier) {
        this.repository = repository;
        this.notifier = notifier;
    }
}

这一行构造函数的改变,带来了三件事:依赖显式化、可替换(测试友好)、可配置(谁提供依赖由外部决定)。

Spring 的容器就是"自动完成这些装配"的那只手。

5. 核心概念 ​

5.1 IOC 与 DI:思想与手段 ​

概念含义
IOC(控制反转)对象创建与依赖装配的控制权,从代码转交到外部
DI(依赖注入)IOC 的实现手段:外部把依赖"注入"进对象

"反转"的是谁:创建对象的控制权。

text
传统:OrderService 自己决定用哪个 Repository(主动 new)
IOC:  由容器决定给 OrderService 注入哪个 Repository(被动接收)

为什么要反转:因为"用哪个实现"是一个会变的外部决策(测试用内存版、生产用数据库版),不该硬编码在业务类里。

5.2 容器长什么样 ​

java
try (AnnotationConfigApplicationContext context =
             new AnnotationConfigApplicationContext(AppConfig.class)) {
    OrderService service = context.getBean(OrderService.class);
    service.place(new Order("ORD-002", 29_900L));
}

ApplicationContext 就是容器——它负责:

职责说明
创建 Bean调用配置里定义的创建方法
装配依赖按类型/名字把 Bean 注入到需要它们的地方
管理生命周期单例的创建时机、销毁回调
提供查找入口getBean(...)

实测:容器启动后里面有 9 个 Bean:

text
容器里的 Bean:9 个
  - org.springframework.context.annotation.internalConfigurationAnnotationProcessor
  - org.springframework.context.annotation.internalAutowiredAnnotationProcessor
  - org.springframework.context.event.internalEventListenerProcessor
  - org.springframework.context.event.internalEventListenerFactory
  - iocLesson.AppConfig
  - orderRepository
  - notifier
  - orderService
  - prototypeBuilder

注意前 4 个:它们不是你定义的,而是 Spring 自己注册的"处理器"——AutowiredAnnotationProcessor 就是"处理 @Autowired 注解"的那个组件(《注解基础》 讲的"注解需要读取方",这里就是活生生的例子)。

这也是容器能力的来源:它先把自己武装好,再处理你的 Bean。

5.3 声明 Bean 的两种方式 ​

方式一:@Configuration + @Bean(显式) ​

java
@Configuration
static class AppConfig {
    @Bean
    OrderRepository orderRepository() {
        return new InMemoryOrderRepository();
    }

    @Bean
    OrderService orderService(OrderRepository repository, Notifier notifier) {
        return new OrderService(repository, notifier);      // 参数由容器注入
    }
}

特点:显式、集中、可读——所有装配关系在一个文件里看得清。

方式二:@Component 系列 + 组件扫描(隐式) ​

java
@Repository
public class JdbcOrderRepository implements OrderRepository { }

@Service
public class OrderService {
    private final OrderRepository repository;

    @Autowired
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

四个"语义化"的注解(功能上等价,区别只在可读性):

注解语义用在
@Component通用组件不属于下面三类的类
@Service业务服务Service 层
@Repository数据访问DAO 层(Spring 会给它加异常转换)
@Controller控制器Web 层

该用哪种:

场景建议
自己写的类(Service、Controller)@Component 系列(少写配置)
第三方库的类(无法加注解)@Configuration + @Bean
需要复杂构造逻辑(如配置数据源)@Configuration + @Bean
需要看清全部装配关系(教学、排查)@Configuration + @Bean

本节用第一种——因为容器做了什么,一眼可见。

5.4 三种注入方式的取舍 ​

java
// ① 构造器注入(推荐)
private final OrderRepository repository;

public OrderService(OrderRepository repository) {
    this.repository = repository;
}

// ② setter 注入
private OrderRepository repository;

@Autowired
public void setRepository(OrderRepository repository) {
    this.repository = repository;
}

// ③ 字段注入(最简洁,但问题最多)
@Autowired
private OrderRepository repository;
维度构造器setter字段
依赖能否为最终字段✅ final❌❌
对象创建后是否完整✅ 一定完整⚠️ 可能"半初始化"⚠️ 同左
能否在纯单元测试里注入✅ 直接 new⚠️ 需要 setter❌ 必须用容器或反射
循环依赖❌ 启动就失败✅ 能绕过✅ 能绕过
代码量中多最少

为什么推荐构造器注入:

  1. 依赖不可变(final)——对象创建后就完整,不会被中途改掉;
  2. 测试友好——new OrderService(mockRepo, mockNotifier) 就够了,不需要启动 Spring(T1-010 的测试就是这么写的);
  3. 循环依赖会被暴露(见 5.6)——这是好事。

字段注入的真实问题(面试高频):

问题说明
无法 final依赖可以被中途替换
无法脱离容器测试必须启动 Spring 或用反射注入
依赖被隐藏一个类注入了 10 个字段,从构造函数完全看不出来
助长"上帝类"加依赖太容易,导致职责越堆越多

所以:新代码用构造器注入;字段注入只在"必须快速改老代码"时使用。

5.5 作用域 ​

作用域行为适用
singleton(默认)容器里只有一个实例无状态的服务、DAO
prototype每次 getBean 都新建有状态的对象(如每次请求的上下文)
request / session每个 HTTP 请求/会话一个Web 应用(需要 Web 环境)

实测:

text
两次 getBean(OrderService) 是同一个对象吗:true
prototype 作用域两次 getBean 是同一个对象吗:false

关键认知:单例 Bean 必须是"无状态"的。

java
@Service
public class OrderService {
    private int counter;              // ✗ 危险:单例里存可变状态

    public void place(Order order) {
        counter++;                    // 多线程下会出错([S1-028](../../01-java-basics/concurrent/S1-028))
    }
}

为什么:所有请求共用同一个实例——一个请求改了 counter,另一个请求会看到。

修复:把状态放在方法参数或局部变量里;确实需要跨请求保存用数据库或缓存。

一种常见的例外:prototype 注入到 singleton 里——注入一次就固定了(不会每次新建),需要 @Lookup 或 ObjectProvider 才能拿到新实例。这是一个容易踩的坑。

5.6 循环依赖(实测) ​

java
@Configuration
static class CircularConfig {
    @Bean
    CircularA circularA(CircularB b) { return new CircularA(b); }

    @Bean
    CircularB circularB(CircularA a) { return new CircularB(a); }    // A ↔ B 互相需要
}

实测输出:

text
启动失败:Error creating bean with name 'circularA': Requested bean is currently in creation:
Is there an unresolvable circular reference or an asynchronous initialization dependency?

完整错误链(Spring 会把循环路径展开给你看):

text
UnsatisfiedDependencyException: Error creating bean with name 'circularA'
  → Unsatisfied dependency expressed through method 'circularA' parameter 0
  → Error creating bean with name 'circularB'
  → Unsatisfied dependency expressed through method 'circularB' parameter 0
  → Error creating bean with name 'circularA': Requested bean is currently in creation

A → B → A,链路清清楚楚。

为什么构造器注入会直接失败:构造器注入要求"依赖先就绪"——A 需要 B 完成、B 需要 A 完成,循环等待,无法"先建一半"。

注入方式循环依赖原因
构造器注入❌ 启动失败构造器要求依赖完整
字段 / setter 注入⚠️ 能启动(用三级缓存)允许"先建空对象、后注入"

为什么"失败"是好事:

循环依赖几乎总意味着"这两个类职责耦合太紧"——它们互相需要对方的信息,说明边界划错了。

Spring 让它在启动时就暴露,比让它在运行时悄悄工作更好。

正确的解法(按推荐顺序):

解法说明
① 重构职责抽出一个第三方类承担共用逻辑(真正的解法)
② 用事件解耦A 发事件、B 监听(《Spring:AOP 与事务》 的 AOP 也能做到)
③ @Lazy注入一个代理,用到时才创建(治标,掩盖了设计问题)
④ setter/字段注入同上,不推荐

6. 递进示例 ​

6.1 最小示例:从容器取 Bean ​

java
try (AnnotationConfigApplicationContext context =
             new AnnotationConfigApplicationContext(AppConfig.class)) {
    OrderService service = context.getBean(OrderService.class);
    service.place(new Order("ORD-002", 29_900L));
}

6.2 业务示例:换实现不改业务类 ​

java
@Configuration
static class AppConfig {
    @Bean
    Notifier notifier() {
        return new ConsoleNotifier();            // 改成 SmsNotifier() 即可,业务类不用动
    }
}

这就是 IOC 的全部价值:把"用哪个实现"从代码里挪到配置里。

6.3 反例:在 Service 里 new 依赖 ​

java
@Service
public class OrderService {
    private final OrderRepository repository = new JdbcOrderRepository();    // ✗
}

三个后果:无法替换(测试困难)、依赖被隐藏、OrderRepository 与 OrderService 硬绑定。

7. 完整可运行代码 ​

文件:labs/spring-core/src/main/java/cn/kb/learn/spring/IocLesson.java(节选)

java
public class IocLesson {
    /** 手动装配版:依赖在类内部 new 出来,无法替换 */
    static class ManualOrderService {
        private final OrderRepository repository = new InMemoryOrderRepository();
        private final Notifier notifier = new ConsoleNotifier();
    }

    /** 容器装配版:依赖由外部注入 */
    static class OrderService {
        private final OrderRepository repository;
        private final Notifier notifier;

        OrderService(OrderRepository repository, Notifier notifier) {
            this.repository = repository;
            this.notifier = notifier;
        }
    }

    @Configuration
    static class AppConfig {
        @Bean
        OrderRepository orderRepository() {
            return new InMemoryOrderRepository();
        }

        @Bean
        Notifier notifier() {
            return new ConsoleNotifier();
        }

        @Bean
        OrderService orderService(OrderRepository repository, Notifier notifier) {
            return new OrderService(repository, notifier);
        }
    }

    private static void containerApproach() {
        try (AnnotationConfigApplicationContext context =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {
            OrderService service = context.getBean(OrderService.class);
            service.place(new Order("ORD-002", 29_900L));
            System.out.println("容器里的 Bean:" + context.getBeanDefinitionCount() + " 个");
        }
    }
}

运行命令:

powershell
cd labs
./mvnw -pl spring-core compile exec:exec -Dexec.mainClass=cn.kb.learn.spring.IocLesson

预期输出:

text
=== 1. 手动装配:依赖写死在类内部 ===
    [通知] 订单已创建:ORD-001
    订单数:1
    问题:想换成「短信通知」或「数据库仓储」,必须改这个类的源码。

=== 2. 容器装配:依赖由外部注入 ===
    [通知] 订单已创建:ORD-002
    订单数:1
    容器里的 Bean:9 个
      - org.springframework.context.annotation.internalConfigurationAnnotationProcessor
      - org.springframework.context.annotation.internalAutowiredAnnotationProcessor
      - org.springframework.context.event.internalEventListenerProcessor
      - org.springframework.context.event.internalEventListenerFactory
      - iocLesson.AppConfig
      - orderRepository
      - notifier
      - orderService
      - prototypeBuilder
    好处:换实现只需改配置(@Bean 方法),业务类一行不用动。

=== 3. 作用域:单例 vs 多例 ===
    两次 getBean(OrderService) 是同一个对象吗:true
    prototype 作用域两次 getBean 是同一个对象吗:false

=== 4. 循环依赖:构造器注入时容器会拒绝启动 ===
    启动失败:Error creating bean with name 'circularA': Requested bean is currently in creation:
    Is there an unresolvable circular reference or an asynchronous initializatio...

三处值得注意:

输出含义
9 个 Bean 里有 4 个是 org.springframework...容器先注册自己的"处理器",再处理你的 Bean
true / false单例是同一份、多例每次新建
循环依赖时容器直接拒绝启动把设计问题暴露在最早的时刻

8. Debug 观察点 ​

  1. 在 new AnnotationConfigApplicationContext(AppConfig.class) 处按 F7 进入:观察容器启动的完整流程(注册 BeanDefinition → 实例化 → 注入 → 初始化)——refresh() 这个方法是 Spring 的心脏;
  2. 观察单例的创建时机:在 AppConfig.orderService() 里打断点,会发现它在容器启动时就被调用了(单例默认"预初始化")——这也解释了"启动慢但运行快";
  3. 观察 Bean 的查找过程:在 context.getBean(OrderService.class) 处进入,看它如何按类型匹配——有多个同类型 Bean 时会报 NoUniqueBeanDefinitionException(可以用 @Qualifier 指定名字);
  4. 观察 prototype 的创建时机:在 prototypeBuilder() 里打断点,会发现它只在 getBean 时才被调用(与单例相反)。

9. 常见错误 ​

错误 1:字段注入 + final 一起用 ​

java
@Autowired
private final OrderRepository repository;      // ✗ 编译都过不了(final 字段必须在构造器里赋值)

修复:用构造器注入。

错误 2:在单例 Bean 里存可变状态 ​

见 5.5 节。这是最隐蔽的一类并发 bug——因为写的时候看起来完全正常。

自查方法:给 Service 类的所有字段加 final,如果能加上,说明没有可变状态 ✓

错误 3:@Autowired(required = false) 掩盖依赖缺失 ​

java
@Autowired(required = false)
private Notifier notifier;                     // 可能为 null

后果:依赖没配好时不是报错,而是运行时 NPE(在某个特定路径上才触发)。

修复:让缺依赖启动就失败(构造器注入默认就是这样)。

错误 4:以为 prototype Bean 注入到单例后会每次新建 ​

java
@Service
public class OrderService {
    @Autowired
    private OrderContext context;              // prototype,但只注入一次
}

现象:期望每次拿到新的 OrderContext,实际每次都是同一个。

原因:注入只发生一次(单例创建时)。

修复:用 @Lookup、ObjectProvider<T> 或 ApplicationContext.getBean(...) 主动获取。

错误 5:组件扫描范围没对上 ​

java
@SpringBootApplication                       // 默认扫描"当前包及子包"
package com.example.app;

// 而 Service 在 com.example.other 包下 → 扫不到 → 启动失败或找不到 Bean

修复:把启动类放在最外层的包,或用 @ComponentScan("com.example") 显式指定。

10. 动手实验 ​

目标:亲手体验"换实现不改代码"。

步骤:

  1. 运行第 7 节的命令,核对输出;
  2. 新加一个实现:写一个 SmsNotifier implements Notifier(打印 [短信]),把 AppConfig.notifier() 改成返回它——确认 OrderService 一行都不用改;
  3. 把 OrderService 改成"手动装配"版本,对比两个类的代码量与可测试性;
  4. 实验循环依赖的三种解法:
    • 给 CircularConfig.circularB 的参数加 @Lazy,确认容器能启动;
    • 给其中一个类加 setter 注入,确认也能启动;
    • 说明为什么这两种都是"绕过"而不是"解决";
  5. 观察 Bean 的创建时机:在 AppConfig 的两个 @Bean 方法里各打一个断点,观察它们的调用顺序(提示:先创建 repository 还是先创建 orderService?为什么?);
  6. 给单例加可变状态:给 OrderService 加一个 private int count 并在 place 里自增,然后连续调用两次 place——观察 count 累加(说明单例是共享的)。

验收方式:能说出第 3 步的对比结论、第 4 步两种解法为什么是"绕过"。

扩展任务:把 ManualOrderService 改造成"可测试"的形式(用手动构造器注入,不引入 Spring),并写一个用 Mockito 替身的单元测试——说明"依赖注入"这个模式的价值不依赖 Spring。

11. 练习题 ​

基础题

  1. 判断:IOC 与 DI 是同一个概念的两种叫法。(对 / 错)
  2. 填空:默认作用域是 ______;要"每次获取都新建"用 ______。
  3. 简答:为什么推荐构造器注入,而不是字段注入?

代码阅读题

java
@Service
public class CartService {
    @Autowired
    private CartRepository repository;

    private int cacheSize = 0;

    public void add(String item) {
        repository.save(item);
        cacheSize++;
    }

    public int cacheSize() {
        return cacheSize;
    }
}
  1. 这段代码有什么问题?在什么场景下会暴露?
  2. 你会怎么改?

编程题

  1. 用 Spring 装配一个"通知系统":
    • 接口 Notifier,三个实现:ConsoleNotifier、SmsNotifier、EmailNotifier;
    • NotificationService 依赖一个 Notifier(构造器注入)和一个 NotifierRegistry(依赖 List<Notifier>);
    • 要求:
      • NotificationService 的默认通知器由配置决定(改配置就能换);
      • NotifierRegistry 能拿到全部三个实现(提示:@Autowired List<Notifier> 或 Map<String, Notifier>);
    • 运行并打印"默认通知器"与"注册表里的全部通知器";
    • 说明:为什么"注册表"用 List 注入,而"默认通知器"用单个注入?如果不加区分会怎样?

综合任务

  1. 为一个"订单系统"设计 IOC 装配方案:
    • 类:OrderService、OrderRepository(接口 + 内存/数据库两个实现)、Notifier、IdGenerator、Clock;
    • 要求:
      • 用 @Configuration + @Bean 显式装配(不要用组件扫描);
      • OrderRepository 在"测试配置"下用内存实现、"生产配置"下用数据库实现(提示:用 @Profile 或两个配置类);
      • Clock 也由容器提供(生产用 Clock.systemDefaultZone(),测试用 Clock.fixed(...));
    • 写出两个配置类并验证"切换配置不改业务代码";
    • 说明:为什么 Clock 也应该交给容器管? 它和 OrderRepository 的注入理由有什么共同点?
    • 额外要求:指出这套设计里"最可能演变成循环依赖"的两个类,并说明如何提前避免。
分级提示

提示 1(方向):第 4 题的关键是"单例 + 可变状态"。

提示 2(关键点):第 6 题的"多个同类型 Bean"注入时,Spring 需要知道你要哪个——List<T> 表示"全都要",T 表示"要一个"(多个候选时会报错)。

提示 3(接近答案):第 7 题的 Clock 与 OrderRepository 的共同点是"值在测试里不可控或需要替换"——这正是注入的判断标准(T1-006 可测试性 讲过)。

12. 参考答案 ​

基础题
  1. 错。IOC 是思想(控制权从代码转到外部),DI 是实现手段(外部把依赖注入进来)。IOC 还可以用别的方式实现(如依赖查找 context.getBean、模板方法回调),DI 只是其中最常用的一种。

顺带一个区分:context.getBean(...) 是"依赖查找"(自己主动去找),构造器注入是"依赖注入"(被动接收)——两者都属于 IOC,但后者更好(依赖关系显式、容易测试)。

  1. singleton;prototype。
  2. 三个理由:
理由说明
依赖不可变可以声明 final,对象创建后就是完整的
测试友好可以直接 new,不需要启动 Spring(见 T1-010 的 Mockito 测试)
循环依赖会暴露构造器注入下循环依赖启动就失败,逼你修设计;字段注入会"悄悄绕过",把问题留到运行时

字段注入唯一的好处是"写得快"——但代价是上面的三条。新代码不要用它。

代码阅读题(第 4、5 题)

第 4 题:两个问题。

问题一:cacheSize 是可变状态,而 @Service 默认是单例。

java
private int cacheSize = 0;
public void add(String item) { ...; cacheSize++; }

所有请求共用同一个 CartService 实例,所以:

场景现象
两个用户同时加购物车cacheSize 被累加(不是各自独立)
并发调用cacheSize++ 不是原子操作(《线程与同步基础》),会丢更新
服务重启计数清零(它本来就只是个内存变量)

问题二:repository 用字段注入。

导致:无法 final、脱离容器无法测试、依赖关系被隐藏。

第 5 题:改法

java
@Service
public class CartService {
    private final CartRepository repository;

    public CartService(CartRepository repository) {       // 构造器注入
        this.repository = repository;
    }

    public void add(String item) {
        repository.save(item);
    }

    /** 需要"计数"这类跨请求状态时,交给外部存储 */
    public int cacheSize() {
        return repository.count();
    }
}

关键判断:"cacheSize 这个状态到底该放在哪?"

需求正确的位置
统计"当前有多少商品"(真实业务数据)数据库(由 repository 提供)
性能优化用的缓存缓存组件(Redis / Caffeine)
临时统计(诊断用)监控指标(Micrometer),且要用原子类型

核心原则:单例 Bean 必须无状态——一旦把状态放进去,就等于在所有请求之间共享了它。

编程题(第 6 题)
java
package cn.kb.learn.spring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import java.util.List;
import java.util.Map;

public class NotifierLesson {
    interface Notifier {
        String name();

        void send(String message);
    }

    static class ConsoleNotifier implements Notifier {
        @Override public String name() { return "console"; }
        @Override public void send(String message) { System.out.println("  [控制台] " + message); }
    }

    static class SmsNotifier implements Notifier {
        @Override public String name() { return "sms"; }
        @Override public void send(String message) { System.out.println("  [短信] " + message); }
    }

    static class EmailNotifier implements Notifier {
        @Override public String name() { return "email"; }
        @Override public void send(String message) { System.out.println("  [邮件] " + message); }
    }

    /** 全部通知器的注册表:注入 List<Notifier> 拿到所有实现 */
    static class NotifierRegistry {
        private final Map<String, Notifier> notifiers;

        NotifierRegistry(List<Notifier> all) {
            this.notifiers = all.stream().collect(
                    java.util.stream.Collectors.toMap(Notifier::name, n -> n));
        }

        void sendAll(String message) {
            notifiers.values().forEach(notifier -> notifier.send(message));
        }

        int size() {
            return notifiers.size();
        }
    }

    static class NotificationService {
        private final Notifier defaultNotifier;      // 单个:由配置决定用哪个
        private final NotifierRegistry registry;     // 集合:全部都要

        NotificationService(Notifier defaultNotifier, NotifierRegistry registry) {
            this.defaultNotifier = defaultNotifier;
            this.registry = registry;
        }

        void notifyDefault(String message) {
            defaultNotifier.send(message);
        }

        void notifyAll(String message) {
            registry.sendAll(message);
        }
    }

    @Configuration
    static class NotifierConfig {
        @Bean
        ConsoleNotifier consoleNotifier() {
            return new ConsoleNotifier();
        }

        @Bean
        SmsNotifier smsNotifier() {
            return new SmsNotifier();
        }

        @Bean
        EmailNotifier emailNotifier() {
            return new EmailNotifier();
        }

        /** 用 @Primary 指定"默认通知器"——换配置就能换默认实现 */
        @Bean
        @Primary
        Notifier defaultNotifier(EmailNotifier emailNotifier) {
            return emailNotifier;
        }

        @Bean
        NotifierRegistry notifierRegistry(List<Notifier> all) {
            return new NotifierRegistry(all);
        }

        @Bean
        NotificationService notificationService(Notifier defaultNotifier, NotifierRegistry registry) {
            return new NotificationService(defaultNotifier, registry);
        }
    }

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context =
                     new AnnotationConfigApplicationContext(NotifierConfig.class)) {
            NotificationService service = context.getBean(NotificationService.class);

            System.out.println("默认通知器发送:");
            service.notifyDefault("订单已创建");

            System.out.println("全部通知器发送:");
            service.notifyAll("系统维护通知");
        }
    }
}

预期输出:

text
默认通知器发送:
  [邮件] 订单已创建
全部通知器发送:
  [控制台] 系统维护通知
  [短信] 系统维护通知
  [邮件] 系统维护通知

为什么"注册表"用 List 注入,而"默认通知器"用单个注入:

注入形式语义用在哪里
List<Notifier>"我全都要"注册表、批量处理、策略枚举
Notifier(单个)"我只要一个"默认实现、主要依赖

如果不加区分会怎样:

java
// ✗ 直接注入单个 Notifier,但有 3 个候选 Bean
NotificationService(Notifier notifier) { }
// → 启动报错:NoUniqueBeanDefinitionException: expected single matching bean but found 3

Spring 的解决顺序:

方式说明
@Primary标记"默认选它"(推荐:不侵入消费方代码)
@Qualifier("beanName")显式指定名字(消费方需要知道 Bean 名)
按参数名匹配参数名与 Bean 名一致时优先(脆弱,不推荐依赖)

这是一个很好的"接口化设计"示范:

  • 加一个新通知器:只需要在 NotifierConfig 里加一个 @Bean——NotificationService、NotifierRegistry 一行都不用改(因为它们是面向 Notifier 接口 + 集合注入写的);
  • 这就是 IOC 的复利:设计得好的装配,让扩展变成"加配置"而不是"改代码"。
综合任务(第 7 题)
java
package cn.kb.learn.spring;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

public class OrderSystemAssembly {
    record Order(String orderNo, long amountInCents, Instant createdAt) {
    }

    interface OrderRepository {
        void save(Order order);

        int count();
    }

    interface Notifier {
        void send(String message);
    }

    interface IdGenerator {
        String next();
    }

    static class InMemoryOrderRepository implements OrderRepository {
        private final List<Order> orders = new ArrayList<>();

        @Override public void save(Order order) { orders.add(order); }
        @Override public int count() { return orders.size(); }
    }

    static class JdbcOrderRepository implements OrderRepository {
        @Override public void save(Order order) {
            System.out.println("    [数据库] 写入订单 " + order.orderNo());
        }

        @Override public int count() { return -1; }        // 演示用
    }

    static class ConsoleNotifier implements Notifier {
        @Override public void send(String message) { System.out.println("    [通知] " + message); }
    }

    static class OrderService {
        private final OrderRepository repository;
        private final Notifier notifier;
        private final IdGenerator idGenerator;
        private final Clock clock;

        OrderService(OrderRepository repository, Notifier notifier,
                     IdGenerator idGenerator, Clock clock) {
            this.repository = repository;
            this.notifier = notifier;
            this.idGenerator = idGenerator;
            this.clock = clock;
        }

        Order place(long amountInCents) {
            Order order = new Order(idGenerator.next(), amountInCents, clock.instant());
            repository.save(order);
            notifier.send("订单已创建:" + order.orderNo() + ",时间:" + order.createdAt());
            return order;
        }
    }

    /** 生产配置:数据库 + 系统时钟 */
    @Configuration
    @Profile("prod")
    static class ProdConfig {
        @Bean OrderRepository orderRepository() { return new JdbcOrderRepository(); }
        @Bean Clock clock() { return Clock.systemDefaultZone(); }
    }

    /** 测试配置:内存实现 + 固定时钟 */
    @Configuration
    @Profile("test")
    static class TestConfig {
        @Bean OrderRepository orderRepository() { return new InMemoryOrderRepository(); }

        @Bean
        Clock clock() {
            return Clock.fixed(Instant.parse("2026-09-21T10:00:00Z"), ZoneId.of("UTC"));
        }
    }

    /** 公共配置:与实现无关的部分 */
    @Configuration
    static class CommonConfig {
        @Bean Notifier notifier() { return new ConsoleNotifier(); }

        @Bean
        IdGenerator idGenerator() {
            AtomicLong counter = new AtomicLong();
            return () -> "ORD-" + counter.incrementAndGet();
        }

        @Bean
        OrderService orderService(OrderRepository repository, Notifier notifier,
                                  IdGenerator idGenerator, Clock clock) {
            return new OrderService(repository, notifier, idGenerator, clock);
        }
    }

    public static void main(String[] args) {
        System.out.println("=== 测试配置(内存 + 固定时钟)===");
        try (var context = new AnnotationConfigApplicationContext()) {
            context.getEnvironment().setActiveProfiles("test");
            context.register(TestConfig.class, CommonConfig.class);
            context.refresh();
            OrderService service = context.getBean(OrderService.class);
            service.place(19_900L);
            service.place(29_900L);
            System.out.println("    订单总数:" + context.getBean(OrderRepository.class).count());
        }

        System.out.println("=== 生产配置(数据库 + 系统时钟)===");
        try (var context = new AnnotationConfigApplicationContext()) {
            context.getEnvironment().setActiveProfiles("prod");
            context.register(ProdConfig.class, CommonConfig.class);
            context.refresh();
            context.getBean(OrderService.class).place(39_900L);
        }
    }
}

运行结果:

text
=== 测试配置(内存 + 固定时钟)===
    [通知] 订单已创建:ORD-1,时间:2026-09-21T10:00:00Z
    [通知] 订单已创建:ORD-2,时间:2026-09-21T10:00:00Z
    订单总数:2
=== 生产配置(数据库 + 系统时钟)===
    [数据库] 写入订单 ORD-1
    [通知] 订单已创建:ORD-1,时间:<真实的当前时间>

注意 OrderService 的代码在两个配置下完全一样——这就是 IOC 的收益。

为什么 Clock 也应该交给容器管:

Clock 与 OrderRepository 的共同点:它们的"值/行为在测试里不可控,或者需要替换。

依赖测试时需要什么交给容器后
OrderRepository换成内存实现(不连库)按 profile 切换
Clock换成固定时间(可复现)按 profile 切换
IdGenerator换成固定/递增的 id按 profile 切换

判断标准(与 T1-006 可测试性 完全一致):

这个依赖的值/行为在测试里可控吗?不可控 → 交给容器(可替换)。

顺带一个设计细节:IdGenerator 用了 AtomicLong(《线程与同步基础》)——因为它是单例 Bean,会被多个线程共享,必须保证线程安全 ✓

"最可能演变成循环依赖"的两个类:

类为什么会循环提前避免
OrderService 与 Notifier如果通知器需要回调订单服务查询详情(Notifier → OrderService),就形成循环通知消息里带全所需数据(如上面的代码把订单号与时间都放进消息),让 Notifier 不需要回查
OrderService 与 OrderRepository如果仓储需要调用服务做校验(反模式)校验放在服务层,仓储只管存取

共同规律:循环依赖往往来自"回调"——A 调用 B、B 又需要 A 的信息。

避免方法:把"需要的信息"作为参数传过去,而不是让被调用方回查(这也叫"参数化调用"或"依赖倒置到数据层")。

13. 掌握标准 ​

  • [ ] 能说清 IOC 与 DI 的关系;
  • [ ] 能用 @Configuration + @Bean 装配对象,并解释 IOC 的实际价值;
  • [ ] 能说出三种注入方式的取舍,并知道为什么推荐构造器注入;
  • [ ] 知道单例 Bean 必须无状态,并能识别"单例里存状态"的代码;
  • [ ] 能看懂循环依赖的报错,并知道"重构职责"才是真正的解法;
  • [ ] 知道容器启动时单例就被创建,prototype 则是每次获取时创建。

14. 扩展资源 ​

15. 下一步学习 ​

《Spring:AOP 与事务》:把日志、耗时、事务这些"到处都要有"的逻辑抽出来。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引