Skip to content

第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数据访问组件
@ControllerMVC 控制器
@RestControllerREST 控制器

这些注解本质上都能注册 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、自动配置都会变得更清晰。