第 42 章:IoC 与 DI
学习目标
- 理解 IoC / DI 是干啥的
- 掌握 Bean 注册与注入
- 避开循环依赖的坑
一、IoC 是啥
IoC(Inversion of Control) = 把对象的创建权交给容器。
java
// ❌ 自己 new:强耦合
public class UserService {
private UserMapper userMapper = new UserMapperImpl(); // 写死实现类
// 想换 Mock?想换实现?得改源码
}
// ✅ IoC:容器帮你 new,塞给你
@Service
public class UserService {
private final UserMapper userMapper; // 容器注入
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
}| 术语 | 含义 |
|---|---|
| IoC | 思想:创建权交给容器 |
| DI | 手段:容器把依赖塞给你 |
一句话:IoC 是目的,DI 是手段。
二、注册 Bean 的三种方式
2.1 注解扫描(业务类首选)
java
@Component // 通用
@Service // 业务层
@Repository // 数据访问层(自动转换 SQL 异常)
@Controller // 控制层
@Configuration // 配置类
public class UserService { }这四个功能完全一样,只是语义化,看名字就知道是哪层。
扫描范围:默认扫描
@SpringBootApplication所在包及其子包。
2.2 @Bean 方法(第三方类首选)
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> t = new RedisTemplate<>();
t.setConnectionFactory(factory);
t.setKeySerializer(new StringRedisSerializer());
return t;
}
}为什么用 @Bean?
RedisTemplate是第三方类,没法在源码上贴@Component。
2.3 @Import(框架/Starter 常用)
java
@Import({MyConfig.class, MyRegistrar.class})三、注入方式对比
java
// ① 构造器注入 ✅ 强烈推荐
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper; // final 不可变
// 只有一个构造器时,@Autowired 可省略(Spring 4.3+)
}
// ② 字段注入 ⚠️ 不推荐
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
}| 方式 | 优点 | 缺点 |
|---|---|---|
| 构造器 ✅ | final 不可变、易测试、能发现循环依赖 | 依赖多时构造器长(坏味道信号) |
| 字段 ❌ | 写起来短 | 无法 final、脱离容器不能测试 |
| Setter | 可选依赖 | 对象可能处于半初始状态 |
⚠️ 坑 1:阿里规约和 Spring 官方都推荐构造器注入。依赖超过 5 个说明这个类职责过多,该拆了。
四、同类型多个 Bean
java
public interface PayService { }
@Service("alipay")
public class AlipayService implements PayService { }
@Service("wechatPay")
public class WechatPayService implements PayService { }
// ❌ 报错:NoUniqueBeanDefinitionException
@Autowired private PayService payService;4 种解决方案:
java
// ① @Qualifier 指定名称
@Autowired @Qualifier("alipay") private PayService payService;
// ② 变量名匹配 Bean 名
@Autowired private PayService alipay;
// ③ @Primary 标记默认
@Service @Primary
public class AlipayService implements PayService { }
// ④ Map 策略模式(企业推荐 ✅)
@Autowired
private Map<String, PayService> payServiceMap; // key = bean 名称
public void pay(String channel) {
payServiceMap.get(channel).pay();
}方案 4 是企业标准做法:新增支付渠道只需加
@Service类,不动已有代码(开闭原则)。
五、Bean 作用域
java
@Service
@Scope("prototype") // 每次获取都新建
public class MyBean { }| 作用域 | 说明 |
|---|---|
singleton(默认) | 容器中只有一个实例 |
prototype | 每次获取都新建 |
request | 每个 HTTP 请求一个 |
session | 每个 Session 一个 |
⚠️ 坑 2:单例 Bean 不要有可变成员变量!所有请求共享同一个实例,会引发线程安全问题。需要请求级数据用方法参数或
ThreadLocal。
java
// ❌ 线程不安全
@Service
public class OrderService {
private String currentUserId; // 多请求会互相覆盖
}六、Bean 生命周期
实例化(构造器)
↓
属性注入(Setter / 字段 / 构造器)
↓
@PostConstruct(初始化)
↓
使用中
↓
@PreDestroy(销毁)java
@Service
public class MyService {
@PostConstruct
public void init() {
// 依赖已就绪,可以预热 / 加载缓存
}
@PreDestroy
public void destroy() {
// 释放连接、刷新缓冲区
}
}七、循环依赖
java
@Service
public class A {
@Autowired private B b;
}
@Service
public class B {
@Autowired private A a;
}Spring 用三级缓存解决(单例 + 字段/Setter 注入):
| 缓存 | 存什么 |
|---|---|
一级 singletonObjects | 成品 Bean |
二级 earlySingletonObjects | 半成品 |
三级 singletonFactories | 工厂(为 AOP 代理准备) |
Spring 解决不了的情况:
| 场景 | 能否解决 |
|---|---|
| 字段 / Setter 注入 + 单例 | ✅ |
| 构造器注入 | ❌(实例化都完不成) |
prototype 作用域 | ❌(不走缓存) |
@Async 标注的 Bean | ❌ |
Spring Boot 2.6+ 默认禁止循环依赖,启动直接报错。
yaml
spring:
main:
allow-circular-references: true # ⚠️ 应急开关⚠️ 坑 3:循环依赖是设计问题的信号。正确做法:抽公共逻辑到第三个类 C,A 和 B 都依赖 C。
八、本章小结
| 要点 | 关键 |
|---|---|
| IoC / DI | 创建权交给容器,容器注入依赖 |
| 注入方式 | 构造器 + @RequiredArgsConstructor |
| 多实现 | Map<String, X> 策略模式 |
| 作用域 | 单例不能有可变成员 |
| 循环依赖 | 构造器注入解决不了,本质是设计问题 |
动手练习
- 策略模式:定义
NotifyService接口,实现邮件/短信/钉钉三种,Map<String, NotifyService>注入 - 观察生命周期:写个 Bean,在
@PostConstruct打印日志,看启动顺序
下一章:第 43 章:AOP 面向切面 →