Appearance
第54课:Spring Core - IoC
🎯 学习目标
- 理解 IoC 和 DI 的本质:对象创建和依赖装配从业务代码转移到容器。
- 掌握 Bean 定义、组件扫描、构造器注入、作用域和生命周期。
- 能解释
@Component、@Service、@Repository、@Configuration、@Bean的区别。 - 能处理多个 Bean 候选、循环依赖、配置属性注入等常见问题。
- 理解 Spring 容器为什么是 Spring Boot、AOP、事务、MVC 的基础。
📖 一、为什么需要 IoC
没有 Spring 时,对象通常自己创建依赖:
java
public class OrderService {
private final OrderRepository orderRepository = new JdbcOrderRepository();
private final PaymentClient paymentClient = new HttpPaymentClient();
}问题:
text
业务类和具体实现强耦合
测试时很难替换依赖
对象创建逻辑散落各处
配置变化需要修改代码
横切能力如事务、AOP 难以统一接入IoC(Inversion of Control,控制反转)的意思是:对象不再自己控制依赖创建,而是交给 Spring 容器。
DI(Dependency Injection,依赖注入)是实现 IoC 的主要方式。
text
IoC 是思想:控制权反转给容器。
DI 是手段:容器把依赖注入到对象中。📖 二、Spring 容器和 Bean
Spring 容器负责管理对象,这些对象叫 Bean。
java
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}OrderService 被 Spring 创建并管理。构造器参数 OrderRepository 也由 Spring 查找并注入。
核心流程:
text
扫描类 -> 解析 Bean 定义 -> 实例化 Bean -> 注入依赖 -> 初始化 -> 放入容器 -> 应用使用Spring Boot 启动时,ApplicationContext 就是核心容器。
📖 三、定义 Bean 的方式
1. 组件扫描
java
@Service
public class UserService {
}
@Repository
public class UserRepository {
}
@Controller
public class UserController {
}常见注解:
| 注解 | 语义 |
|---|---|
@Component | 通用组件 |
@Service | 业务服务 |
@Repository | 数据访问组件 |
@Controller | MVC 控制器 |
@RestController | REST 控制器 |
这些注解本质上都能注册 Bean,但语义不同,应该按职责使用。
2. 配置类 + @Bean
第三方类无法加 @Component 时,用 @Bean:
java
@Configuration
public class HttpClientConfig {
@Bean
public HttpClient httpClient() {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
}
}适合:
text
第三方 SDK
需要复杂构造逻辑的对象
依赖外部配置创建的对象3. XML 配置
老项目可能还会看到:
xml
<bean id="userService" class="com.example.UserService">
<property name="userRepository" ref="userRepository"/>
</bean>新项目通常使用注解和 Java 配置。
📖 四、依赖注入方式
1. 构造器注入(推荐)
java
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {
this.orderRepository = orderRepository;
this.paymentClient = paymentClient;
}
}优点:
text
依赖不可变
对象创建后就是完整状态
便于单元测试
可以暴露循环依赖问题
不依赖反射修改私有字段配合 Lombok:
java
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
}2. Setter 注入
java
@Service
public class ReportService {
private ExportClient exportClient;
@Autowired
public void setExportClient(ExportClient exportClient) {
this.exportClient = exportClient;
}
}适合可选依赖,但必需依赖不推荐。
3. 字段注入(不推荐)
java
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
}缺点:
text
无法用 final 表达必需依赖
单元测试不方便
隐藏依赖关系
容易形成过胖类📖 五、多个 Bean 候选
如果一个接口有多个实现:
java
public interface PayClient {
void pay();
}
@Component
public class AlipayClient implements PayClient {
}
@Component
public class WechatPayClient implements PayClient {
}直接注入会失败:
java
public OrderService(PayClient payClient) {
}解决方式一:@Qualifier
java
public OrderService(@Qualifier("alipayClient") PayClient payClient) {
this.payClient = payClient;
}解决方式二:@Primary
java
@Primary
@Component
public class AlipayClient implements PayClient {
}解决方式三:注入全部实现做策略路由:
java
@Service
public class PayRouter {
private final Map<String, PayClient> clients;
public PayRouter(Map<String, PayClient> clients) {
this.clients = clients;
}
}📖 六、Bean 作用域
| 作用域 | 含义 |
|---|---|
| singleton | 默认,一个容器一个实例 |
| prototype | 每次获取创建新实例 |
| request | 每个 HTTP 请求一个实例 |
| session | 每个 HTTP Session 一个实例 |
示例:
java
@Component
@Scope("prototype")
public class ExportTaskContext {
}大多数 Service、Repository 都应该是 singleton。它们必须是无状态或线程安全的。
错误示例:
java
@Service
public class UserService {
private Long currentUserId;
}Spring 单例服务被多线程共享,保存请求级状态会串数据。
📖 七、Bean 生命周期
java
@Component
public class CacheWarmup {
@PostConstruct
public void init() {
// Bean 创建并注入依赖后执行
}
@PreDestroy
public void destroy() {
// 容器关闭前执行
}
}更完整的生命周期包括:
text
实例化
属性注入
Aware 回调
BeanPostProcessor 前置处理
初始化方法
BeanPostProcessor 后置处理
放入容器
销毁回调BeanPostProcessor 是 Spring 扩展点,AOP 代理、自动注入等很多能力都和它有关。
📖 八、配置属性注入
简单配置:
java
@Value("${payment.timeout:3000}")
private int timeout;复杂配置推荐 @ConfigurationProperties:
java
@ConfigurationProperties(prefix = "payment")
public class PaymentProperties {
private String endpoint;
private int timeout;
}注册:
java
@EnableConfigurationProperties(PaymentProperties.class)
@Configuration
public class PaymentConfig {
}@ConfigurationProperties 更适合结构化配置,也更容易做类型安全和校验。
⚠️ 九、常见陷阱
1. 循环依赖
text
A 依赖 B
B 依赖 A构造器注入会直接暴露问题。不要用字段注入掩盖设计问题。通常应该抽出第三个服务,或重新划分职责。
2. Bean 名称冲突
不同包里同名类默认 Bean 名可能相同。必要时显式命名:
java
@Service("orderQueryService")
public class OrderService {
}3. @Configuration 和普通类混淆
@Configuration 类中的 @Bean 方法会被 Spring 代理,保证 singleton 语义。普通 @Component 中也能写 @Bean,但语义和代理行为不同,新手不要混用。
4. 单例 Bean 保存可变状态
Service 默认单例,多线程共享。请求级数据放到方法参数、局部变量、数据库或专门上下文中,不要放字段。
🆚 十、Java vs C 对比
| 维度 | C 常见方式 | Spring IoC |
|---|---|---|
| 对象创建 | 手动 malloc/初始化函数 | 容器统一创建 Bean |
| 依赖管理 | 手动传指针或全局变量 | 构造器注入 |
| 配置切换 | 编译宏、配置文件手写解析 | Profile、Properties、Bean 条件 |
| 生命周期 | 手动 init/destroy | 容器回调 |
| 扩展能力 | 函数指针、回调表 | BeanPostProcessor、AOP、事件 |
IoC 可以理解为把“对象图构建”从业务代码中抽出来,由容器集中管理。
💡 十一、最佳实践
- 必需依赖使用构造器注入,并尽量用
final字段。 - Service、Repository 默认保持无状态。
- 第三方 SDK 客户端用
@Bean管理,集中配置超时和连接池。 - 多实现接口用
@Qualifier、@Primary或 Map 策略路由。 - 配置项多时使用
@ConfigurationProperties。 - 不要用字段注入掩盖类依赖过多的问题。
- 发现循环依赖时优先重构职责,而不是寻找绕过开关。
- 理解 IoC 后再学习 AOP、事务、MVC,会更容易理解 Spring 的行为。
🧭 十二、项目落地清单
检查一个 Spring 项目的 IoC 使用是否健康:
text
Service 是否大多通过构造器注入?
是否存在大量字段注入?
是否有 Bean 保存请求级状态?
多个实现注入是否明确?
第三方客户端是否统一由配置类创建?
配置项是否集中建模?
循环依赖是否被设计重构解决?
Bean 命名是否清晰?🔍 十三、自测问题
text
IoC 和 DI 有什么区别?
为什么构造器注入优于字段注入?
@Component 和 @Bean 分别适合什么场景?
singleton Bean 为什么不能保存请求状态?
多个实现类注入同一个接口时会发生什么?
@Primary 和 @Qualifier 有什么区别?
Bean 生命周期中 @PostConstruct 什么时候执行?
为什么循环依赖通常是设计信号?🎓 小结
Spring IoC 是整个 Spring 生态的地基。它不只是帮你少写 new,更重要的是统一对象创建、依赖装配、生命周期和扩展点。理解 IoC 后,AOP、事务、MVC、自动配置都会变得更清晰。