第 42 章:IoC 控制反转
学习目标
- 理解 IoC 与 DI 的区别
- 掌握 Bean 的注册与注入方式
- 理解 Bean 生命周期与作用域
- 掌握循环依赖的原理与解决
一、没有 IoC 的世界
// ❌ 强耦合:UserService 自己 new 依赖
public class UserService {
private UserMapper userMapper = new UserMapperImpl(); // ① 写死了实现类
private EmailSender sender = new AliyunEmailSender(); // ② 换阿里云→腾讯云要改代码
}问题:
- 换实现类要改源码
- 单元测试没法替换成 Mock
- 依赖的依赖也要自己 new(
new AliyunEmailSender(new Config(new Properties())))
二、IoC 的思路
控制反转:对象的创建权从「自己 new」反转给「容器」。
| 术语 | 含义 |
|---|---|
| IoC(控制反转) | 思想:把创建权交给容器 |
| DI(依赖注入) | 实现手段:容器把依赖塞给你 |
一句话:IoC 是目的,DI 是手段。
三、注册 Bean 的三种方式
① 注解扫描(业务类首选)
@Component // 通用组件
@Service // 业务层(语义化的 @Component)
@Repository // 数据访问层
@Controller // 控制层
@Configuration // 配置类
public class UserService { }这四个语义注解功能完全一样,只是让人一眼看出这个类属于哪一层。
@Repository额外有个能力:把数据库异常转成 Spring 统一的DataAccessException。
扫描范围:默认扫描 @SpringBootApplication 所在包及其子包。
@SpringBootApplication(scanBasePackages = {"com.taskflow", "com.common"})② @Bean 方法(第三方类首选)
@Configuration
public class RedisConfig {
@Bean // ① 方法返回值成为 Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) { // ② 方法参数自动注入
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
return template;
}
}为什么第三方类要用
@Bean?RedisTemplate是 Spring 提供的类,你没法在它源码上加@Component。
③ @Import(框架/Starter 常用)
@Import({MyConfig.class, MyRegistrar.class})四、注入方式对比
// ① 构造器注入 ✅ 强烈推荐
@Service
public class UserService {
private final UserMapper userMapper; // ② final:保证不可变
// ③ 只有一个构造器时 @Autowired 可省略(Spring 4.3+)
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
}
// ④ Lombok 简化:等价于上面
@Service
@RequiredArgsConstructor // 为所有 final 字段生成构造器
public class UserService {
private final UserMapper userMapper;
}
// ⑤ 字段注入 ⚠️ 不推荐
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
}
// ⑥ Setter 注入:可选依赖时用
@Autowired(required = false)
public void setCache(CacheService cache) { }| 方式 | 优点 | 缺点 |
|---|---|---|
| 构造器 ✅ | 依赖不可变、不会 NPE、易测试、能发现循环依赖 | 依赖多时构造器长(但这本身是坏味道信号) |
| 字段 ❌ | 写起来短 | 无法 final、脱离容器无法测试、隐藏依赖过多 |
| Setter | 支持可选依赖 | 对象可能处于半初始化状态 |
为什么阿里规约和 Spring 官方都推荐构造器注入?
final保证依赖不会被中途替换- 单元测试可以直接
new UserService(mockMapper),不需要启动 Spring- 如果一个类构造器参数超过 5 个,说明它职责过多——这个「痛感」是有价值的设计反馈
五、同类型多个 Bean
public interface PayService { }
@Service("alipay")
public class AlipayService implements PayService { }
@Service("wechatPay")
public class WechatPayService implements PayService { }
// ❌ 报错:NoUniqueBeanDefinitionException
@Autowired
private PayService payService;
// ✅ 方案 1:@Qualifier 指定名称
@Autowired
@Qualifier("alipay")
private PayService payService;
// ✅ 方案 2:变量名匹配 Bean 名
@Autowired
private PayService alipay;
// ✅ 方案 3:@Primary 标记默认实现
@Service
@Primary
public class AlipayService implements PayService { }
// ✅ 方案 4:全部注入,运行时按策略选(策略模式)
@Autowired
private Map<String, PayService> payServiceMap; // key = bean 名称
public void pay(String channel) {
payServiceMap.get(channel).pay();
}方案 4 是企业开发的标准做法:新增支付渠道只需加一个
@Service类,不用改任何已有代码(开闭原则)。
六、Bean 作用域
@Service
@Scope("prototype")
public class MyBean { }| 作用域 | 说明 |
|---|---|
singleton | 默认,容器中只有一个实例 |
prototype | 每次获取都新建 |
request | 每个 HTTP 请求一个(Web) |
session | 每个 Session 一个(Web) |
⚠️ 单例 Bean 不要有可变成员变量!所有请求共享同一个实例,成员变量会造成线程安全问题。需要请求级数据用方法参数或
ThreadLocal。
// ❌ 线程不安全
@Service
public class OrderService {
private String currentUserId; // ❌ 多请求会互相覆盖
public void process(String userId) {
this.currentUserId = userId;
doWork();
}
}七、Bean 生命周期
@Service
public class MyService {
public MyService() {
System.out.println("1. 构造器");
}
@Autowired
public void setDep(OtherService dep) {
System.out.println("2. 依赖注入");
}
@PostConstruct // ① 初始化:此时依赖已注入完毕
public void init() {
System.out.println("3. 初始化:可以做预热、加载缓存");
}
@PreDestroy // ② 销毁:应用关闭前执行
public void destroy() {
System.out.println("4. 销毁:释放连接、刷新缓冲区");
}
}为什么不能在构造器里用依赖? 构造器执行时依赖还没注入(除非是构造器注入)。需要「依赖就绪后做初始化」就用
@PostConstruct。
八、循环依赖
@Service
public class A {
@Autowired private B b;
}
@Service
public class B {
@Autowired private A a;
}三级缓存解决
| 缓存 | 内容 |
|---|---|
一级 singletonObjects | 完整的成品 Bean |
二级 earlySingletonObjects | 半成品(已实例化,未注入属性) |
三级 singletonFactories | 生成半成品的工厂(为 AOP 代理准备) |
为什么要三级不是两级? 如果 A 需要被 AOP 代理,注入给 B 的必须是代理对象而不是原始对象。三级缓存存的是工厂,调用时才决定「返回原始对象还是代理对象」。
Spring 解决不了的情况
| 场景 | 能否解决 |
|---|---|
| 字段注入 / Setter 注入 + 单例 | ✅ 能 |
| 构造器注入 | ❌ 不能(实例化都完不成,没法放进缓存) |
prototype 作用域 | ❌ 不能(不走缓存) |
@Async 标注的 Bean | ❌ 常出问题 |
Spring Boot 2.6+ 默认禁止循环依赖,启动直接报错:
spring:
main:
allow-circular-references: true # ⚠️ 应急开关,不要长期开着正确做法:循环依赖是设计问题的信号。应该提取公共逻辑到第三个类 C,让 A 和 B 都依赖 C。
九、本章小结
| 要点 | 关键 |
|---|---|
| IoC | 创建权交给容器 |
| DI | 容器注入依赖 |
| 注册 Bean | @Component 系列 / @Bean / @Import |
| 注入方式 | 构造器注入 + @RequiredArgsConstructor |
| 多实现 | Map<String, XxxService> 策略模式 |
| 作用域 | 默认单例,不要有可变成员 |
| 生命周期 | 构造 → 注入 → @PostConstruct → 使用 → @PreDestroy |
| 循环依赖 | 三级缓存;构造器注入解决不了;本质是设计问题 |
动手练习
练习 1:基础题
实现一个「消息通知」策略:定义 NotifyService 接口,实现邮件、短信、钉钉三种,用 Map 注入,根据参数动态选择。
练习 2:进阶题
写一个 Bean 实现 BeanPostProcessor,打印所有 Bean 的初始化顺序和耗时,找出启动最慢的 Top 5 Bean。
下一章:第 43 章:AOP 面向切面 →