主题
界面
主题
到目前为止写的代码,依赖都是自己 new 出来的:
private final OrderRepository repository = new InMemoryOrderRepository();本节要解决的就是这一行的两个问题:换实现要改源码、测试时要改源码。
Spring 的答案是把"创建与装配对象"这件事交给容器——这就是 IOC(控制反转)。本节用纯 Spring(不上 Boot)把容器的行为看清楚。
完成本节后,你应该能够:
@Configuration + @Bean 装配对象,并解释"为什么换实现不用改业务类";prototype 作用域的区别;看一段"典型的新手代码":
public class OrderService {
private final OrderRepository repository = new JdbcOrderRepository("jdbc:mysql://...");
private final Notifier notifier = new SmsNotifier("key-123");
}三个问题:
| 问题 | 后果 |
|---|---|
| 换实现要改源码 | 测试时想换成内存实现?改代码。上线换短信服务商?改代码。 |
| 无法在测试里替换 | 单元测试被迫连真实数据库(T1-010 测试替身) |
| 依赖关系被隐藏 | 看构造函数不知道这个类需要什么——依赖散落在字段初始化里 |
改成注入后的样子:
public class OrderService {
private final OrderRepository repository;
private final Notifier notifier;
public OrderService(OrderRepository repository, Notifier notifier) {
this.repository = repository;
this.notifier = notifier;
}
}这一行构造函数的改变,带来了三件事:依赖显式化、可替换(测试友好)、可配置(谁提供依赖由外部决定)。
Spring 的容器就是"自动完成这些装配"的那只手。
| 概念 | 含义 |
|---|---|
| IOC(控制反转) | 对象创建与依赖装配的控制权,从代码转交到外部 |
| DI(依赖注入) | IOC 的实现手段:外部把依赖"注入"进对象 |
"反转"的是谁:创建对象的控制权。
传统:OrderService 自己决定用哪个 Repository(主动 new)
IOC: 由容器决定给 OrderService 注入哪个 Repository(被动接收)为什么要反转:因为"用哪个实现"是一个会变的外部决策(测试用内存版、生产用数据库版),不该硬编码在业务类里。
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:
容器里的 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。
@Configuration + @Bean(显式) @Configuration
static class AppConfig {
@Bean
OrderRepository orderRepository() {
return new InMemoryOrderRepository();
}
@Bean
OrderService orderService(OrderRepository repository, Notifier notifier) {
return new OrderService(repository, notifier); // 参数由容器注入
}
}特点:显式、集中、可读——所有装配关系在一个文件里看得清。
@Component 系列 + 组件扫描(隐式) @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 |
本节用第一种——因为容器做了什么,一眼可见。
// ① 构造器注入(推荐)
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 | ❌ 必须用容器或反射 |
| 循环依赖 | ❌ 启动就失败 | ✅ 能绕过 | ✅ 能绕过 |
| 代码量 | 中 | 多 | 最少 |
为什么推荐构造器注入:
final)——对象创建后就完整,不会被中途改掉;new OrderService(mockRepo, mockNotifier) 就够了,不需要启动 Spring(T1-010 的测试就是这么写的);字段注入的真实问题(面试高频):
| 问题 | 说明 |
|---|---|
无法 final | 依赖可以被中途替换 |
| 无法脱离容器测试 | 必须启动 Spring 或用反射注入 |
| 依赖被隐藏 | 一个类注入了 10 个字段,从构造函数完全看不出来 |
| 助长"上帝类" | 加依赖太容易,导致职责越堆越多 |
所以:新代码用构造器注入;字段注入只在"必须快速改老代码"时使用。
| 作用域 | 行为 | 适用 |
|---|---|---|
singleton(默认) | 容器里只有一个实例 | 无状态的服务、DAO |
prototype | 每次 getBean 都新建 | 有状态的对象(如每次请求的上下文) |
request / session | 每个 HTTP 请求/会话一个 | Web 应用(需要 Web 环境) |
实测:
两次 getBean(OrderService) 是同一个对象吗:true
prototype 作用域两次 getBean 是同一个对象吗:false关键认知:单例 Bean 必须是"无状态"的。
@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 才能拿到新实例。这是一个容易踩的坑。
@Configuration
static class CircularConfig {
@Bean
CircularA circularA(CircularB b) { return new CircularA(b); }
@Bean
CircularB circularB(CircularA a) { return new CircularB(a); } // A ↔ B 互相需要
}实测输出:
启动失败:Error creating bean with name 'circularA': Requested bean is currently in creation:
Is there an unresolvable circular reference or an asynchronous initialization dependency?完整错误链(Spring 会把循环路径展开给你看):
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 creationA → B → A,链路清清楚楚。
为什么构造器注入会直接失败:构造器注入要求"依赖先就绪"——A 需要 B 完成、B 需要 A 完成,循环等待,无法"先建一半"。
| 注入方式 | 循环依赖 | 原因 |
|---|---|---|
| 构造器注入 | ❌ 启动失败 | 构造器要求依赖完整 |
| 字段 / setter 注入 | ⚠️ 能启动(用三级缓存) | 允许"先建空对象、后注入" |
为什么"失败"是好事:
循环依赖几乎总意味着"这两个类职责耦合太紧"——它们互相需要对方的信息,说明边界划错了。
Spring 让它在启动时就暴露,比让它在运行时悄悄工作更好。
正确的解法(按推荐顺序):
| 解法 | 说明 |
|---|---|
| ① 重构职责 | 抽出一个第三方类承担共用逻辑(真正的解法) |
| ② 用事件解耦 | A 发事件、B 监听(《Spring:AOP 与事务》 的 AOP 也能做到) |
③ @Lazy | 注入一个代理,用到时才创建(治标,掩盖了设计问题) |
| ④ setter/字段注入 | 同上,不推荐 |
try (AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class)) {
OrderService service = context.getBean(OrderService.class);
service.place(new Order("ORD-002", 29_900L));
}@Configuration
static class AppConfig {
@Bean
Notifier notifier() {
return new ConsoleNotifier(); // 改成 SmsNotifier() 即可,业务类不用动
}
}这就是 IOC 的全部价值:把"用哪个实现"从代码里挪到配置里。
@Service
public class OrderService {
private final OrderRepository repository = new JdbcOrderRepository(); // ✗
}三个后果:无法替换(测试困难)、依赖被隐藏、OrderRepository 与 OrderService 硬绑定。
文件:labs/spring-core/src/main/java/cn/kb/learn/spring/IocLesson.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() + " 个");
}
}
}运行命令:
cd labs
./mvnw -pl spring-core compile exec:exec -Dexec.mainClass=cn.kb.learn.spring.IocLesson预期输出:
=== 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 | 单例是同一份、多例每次新建 |
| 循环依赖时容器直接拒绝启动 | 把设计问题暴露在最早的时刻 |
new AnnotationConfigApplicationContext(AppConfig.class) 处按 F7 进入:观察容器启动的完整流程(注册 BeanDefinition → 实例化 → 注入 → 初始化)——refresh() 这个方法是 Spring 的心脏;AppConfig.orderService() 里打断点,会发现它在容器启动时就被调用了(单例默认"预初始化")——这也解释了"启动慢但运行快";context.getBean(OrderService.class) 处进入,看它如何按类型匹配——有多个同类型 Bean 时会报 NoUniqueBeanDefinitionException(可以用 @Qualifier 指定名字);prototype 的创建时机:在 prototypeBuilder() 里打断点,会发现它只在 getBean 时才被调用(与单例相反)。final 一起用 @Autowired
private final OrderRepository repository; // ✗ 编译都过不了(final 字段必须在构造器里赋值)修复:用构造器注入。
见 5.5 节。这是最隐蔽的一类并发 bug——因为写的时候看起来完全正常。
自查方法:给 Service 类的所有字段加 final,如果能加上,说明没有可变状态 ✓
@Autowired(required = false) 掩盖依赖缺失 @Autowired(required = false)
private Notifier notifier; // 可能为 null后果:依赖没配好时不是报错,而是运行时 NPE(在某个特定路径上才触发)。
修复:让缺依赖启动就失败(构造器注入默认就是这样)。
prototype Bean 注入到单例后会每次新建 @Service
public class OrderService {
@Autowired
private OrderContext context; // prototype,但只注入一次
}现象:期望每次拿到新的 OrderContext,实际每次都是同一个。
原因:注入只发生一次(单例创建时)。
修复:用 @Lookup、ObjectProvider<T> 或 ApplicationContext.getBean(...) 主动获取。
@SpringBootApplication // 默认扫描"当前包及子包"
package com.example.app;
// 而 Service 在 com.example.other 包下 → 扫不到 → 启动失败或找不到 Bean修复:把启动类放在最外层的包,或用 @ComponentScan("com.example") 显式指定。
目标:亲手体验"换实现不改代码"。
步骤:
SmsNotifier implements Notifier(打印 [短信]),把 AppConfig.notifier() 改成返回它——确认 OrderService 一行都不用改;OrderService 改成"手动装配"版本,对比两个类的代码量与可测试性;CircularConfig.circularB 的参数加 @Lazy,确认容器能启动;AppConfig 的两个 @Bean 方法里各打一个断点,观察它们的调用顺序(提示:先创建 repository 还是先创建 orderService?为什么?);OrderService 加一个 private int count 并在 place 里自增,然后连续调用两次 place——观察 count 累加(说明单例是共享的)。验收方式:能说出第 3 步的对比结论、第 4 步两种解法为什么是"绕过"。
扩展任务:把 ManualOrderService 改造成"可测试"的形式(用手动构造器注入,不引入 Spring),并写一个用 Mockito 替身的单元测试——说明"依赖注入"这个模式的价值不依赖 Spring。
基础题
代码阅读题
@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;
}
}编程题
Notifier,三个实现:ConsoleNotifier、SmsNotifier、EmailNotifier;NotificationService 依赖一个 Notifier(构造器注入)和一个 NotifierRegistry(依赖 List<Notifier>);NotificationService 的默认通知器由配置决定(改配置就能换);NotifierRegistry 能拿到全部三个实现(提示:@Autowired List<Notifier> 或 Map<String, Notifier>);List 注入,而"默认通知器"用单个注入?如果不加区分会怎样?综合任务
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 可测试性 讲过)。
context.getBean、模板方法回调),DI 只是其中最常用的一种。顺带一个区分:context.getBean(...) 是"依赖查找"(自己主动去找),构造器注入是"依赖注入"(被动接收)——两者都属于 IOC,但后者更好(依赖关系显式、容易测试)。
singleton;prototype。| 理由 | 说明 |
|---|---|
| 依赖不可变 | 可以声明 final,对象创建后就是完整的 |
| 测试友好 | 可以直接 new,不需要启动 Spring(见 T1-010 的 Mockito 测试) |
| 循环依赖会暴露 | 构造器注入下循环依赖启动就失败,逼你修设计;字段注入会"悄悄绕过",把问题留到运行时 |
字段注入唯一的好处是"写得快"——但代价是上面的三条。新代码不要用它。
第 4 题:两个问题。
问题一:cacheSize 是可变状态,而 @Service 默认是单例。
private int cacheSize = 0;
public void add(String item) { ...; cacheSize++; }所有请求共用同一个 CartService 实例,所以:
| 场景 | 现象 |
|---|---|
| 两个用户同时加购物车 | cacheSize 被累加(不是各自独立) |
| 并发调用 | cacheSize++ 不是原子操作(《线程与同步基础》),会丢更新 |
| 服务重启 | 计数清零(它本来就只是个内存变量) |
问题二:repository 用字段注入。
导致:无法 final、脱离容器无法测试、依赖关系被隐藏。
第 5 题:改法
@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 必须无状态——一旦把状态放进去,就等于在所有请求之间共享了它。
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("系统维护通知");
}
}
}预期输出:
默认通知器发送:
[邮件] 订单已创建
全部通知器发送:
[控制台] 系统维护通知
[短信] 系统维护通知
[邮件] 系统维护通知为什么"注册表"用 List 注入,而"默认通知器"用单个注入:
| 注入形式 | 语义 | 用在哪里 |
|---|---|---|
List<Notifier> | "我全都要" | 注册表、批量处理、策略枚举 |
Notifier(单个) | "我只要一个" | 默认实现、主要依赖 |
如果不加区分会怎样:
// ✗ 直接注入单个 Notifier,但有 3 个候选 Bean
NotificationService(Notifier notifier) { }
// → 启动报错:NoUniqueBeanDefinitionException: expected single matching bean but found 3Spring 的解决顺序:
| 方式 | 说明 |
|---|---|
@Primary | 标记"默认选它"(推荐:不侵入消费方代码) |
@Qualifier("beanName") | 显式指定名字(消费方需要知道 Bean 名) |
| 按参数名匹配 | 参数名与 Bean 名一致时优先(脆弱,不推荐依赖) |
这是一个很好的"接口化设计"示范:
NotifierConfig 里加一个 @Bean——NotificationService、NotifierRegistry 一行都不用改(因为它们是面向 Notifier 接口 + 集合注入写的);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);
}
}
}运行结果:
=== 测试配置(内存 + 固定时钟)===
[通知] 订单已创建: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 的信息。
避免方法:把"需要的信息"作为参数传过去,而不是让被调用方回查(这也叫"参数化调用"或"依赖倒置到数据层")。
@Configuration + @Bean 装配对象,并解释 IOC 的实际价值;prototype 则是每次获取时创建。《Spring:AOP 与事务》:把日志、耗时、事务这些"到处都要有"的逻辑抽出来。