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

第 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> 策略模式
作用域单例不能有可变成员
循环依赖构造器注入解决不了,本质是设计问题

动手练习 ​

  1. 策略模式:定义 NotifyService 接口,实现邮件/短信/钉钉三种,Map<String, NotifyService> 注入
  2. 观察生命周期:写个 Bean,在 @PostConstruct 打印日志,看启动顺序

下一章:第 43 章:AOP 面向切面 →

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