Skip to content
第 42 / 250 章后端⏱ 10 分钟阅读

第 42 章:IoC 控制反转

学习目标

  • 理解 IoC 与 DI 的区别
  • 掌握 Bean 的注册与注入方式
  • 理解 Bean 生命周期与作用域
  • 掌握循环依赖的原理与解决

一、没有 IoC 的世界

java
// ❌ 强耦合:UserService 自己 new 依赖
public class UserService {
    private UserMapper userMapper = new UserMapperImpl();   // ① 写死了实现类
    private EmailSender sender = new AliyunEmailSender();   // ② 换阿里云→腾讯云要改代码
}

问题

  1. 换实现类要改源码
  2. 单元测试没法替换成 Mock
  3. 依赖的依赖也要自己 new(new AliyunEmailSender(new Config(new Properties()))

二、IoC 的思路

控制反转:对象的创建权从「自己 new」反转给「容器」。

术语含义
IoC(控制反转)思想:把创建权交给容器
DI(依赖注入)实现手段:容器把依赖塞给你

一句话:IoC 是目的,DI 是手段。

三、注册 Bean 的三种方式

① 注解扫描(业务类首选)

java
@Component      // 通用组件
@Service        // 业务层(语义化的 @Component)
@Repository     // 数据访问层
@Controller     // 控制层
@Configuration  // 配置类
public class UserService { }

这四个语义注解功能完全一样,只是让人一眼看出这个类属于哪一层。@Repository 额外有个能力:把数据库异常转成 Spring 统一的 DataAccessException

扫描范围:默认扫描 @SpringBootApplication 所在包及其子包。

java
@SpringBootApplication(scanBasePackages = {"com.taskflow", "com.common"})

② @Bean 方法(第三方类首选)

java
@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 常用)

java
@Import({MyConfig.class, MyRegistrar.class})

四、注入方式对比

java
// ① 构造器注入 ✅ 强烈推荐
@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 官方都推荐构造器注入?

  1. final 保证依赖不会被中途替换
  2. 单元测试可以直接 new UserService(mockMapper),不需要启动 Spring
  3. 如果一个类构造器参数超过 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;

// ✅ 方案 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 作用域

java
@Service
@Scope("prototype")
public class MyBean { }
作用域说明
singleton默认,容器中只有一个实例
prototype每次获取都新建
request每个 HTTP 请求一个(Web)
session每个 Session 一个(Web)

⚠️ 单例 Bean 不要有可变成员变量!所有请求共享同一个实例,成员变量会造成线程安全问题。需要请求级数据用方法参数或 ThreadLocal

java
// ❌ 线程不安全
@Service
public class OrderService {
    private String currentUserId;      // ❌ 多请求会互相覆盖

    public void process(String userId) {
        this.currentUserId = userId;
        doWork();
    }
}

七、Bean 生命周期

java
@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

八、循环依赖

java
@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+ 默认禁止循环依赖,启动直接报错:

yaml
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 面向切面

本站基于 VitePress 构建 · 由 Codebook 团队维护