一、先说新手最容易混淆的核心点:
private UserMapper userMapper; 只是声明了一个变量,并没有创建 / 初始化这个对象 ------ 变量和对象的区别,正是理解依赖注入的关键。
一、先看反例:只写 private UserMapper userMapper; 会发生什么?
@Service
public class UserService {
// 只声明变量,不做任何注入
private UserMapper userMapper;
public User getUserById(Long id) {
// 调用 userMapper 的方法
return userMapper.selectById(id);
}
}
当你运行这段代码,调用 getUserById 时,会直接抛出 NullPointerException(空指针异常)。
核心原因:
private UserMapper userMapper; 只是告诉 JVM:"我需要一个叫 userMapper 的变量,类型是 UserMapper",但 JVM 只会给这个变量分配一个 "空引用"(值为 null),并没有创建 UserMapper 的实例对象 。
就像:
- 你说 "我需要一辆车"(声明变量
private Car car;); - 但你手里并没有真的车(变量值为
null); - 你试图 "开车"(调用
car.drive()),自然会失败。
二、为什么必须 "注入"?注入的本质是什么?
UserMapper不是普通的类 ------ 它是 MyBatis 生成的代理类,需要 Spring 容器创建、初始化(比如绑定 SQL 会话、数据源),你自己无法通过new UserMapper()创建可用的实例 (因为UserMapper是接口,不是具体类)。
注入的本质:
把 Spring 容器中已经创建好的、可用的 UserMapper 对象,赋值给 UserService 中的 userMapper 变量 ,让这个变量从 null 变成一个真实的、能干活的对象。
简单来说:声明变量 = 画饼,注入 = 把饼拿到手,只画饼是吃不饱的
在 Spring Boot 项目中,依赖注入(DI)是控制反转(IoC)的核心实现方式,而构造器注入是 Spring 官方明确推荐的最佳实践。下面从注入方式对比、官方推荐原因、实际应用场景三个维度,用新手能理解的方式讲透。
二、Spring 中 3 种核心依赖注入方式
先明确:依赖注入的本质是让 Spring 容器帮我们创建对象时,自动填充对象的依赖属性 ,而非手动 new 依赖对象。
1. 构造器注入(Constructor Injection)
通过类的构造方法注入依赖,结合 @Autowired(Spring 4.3+ 后,若类只有一个构造器,可省略 @Autowired):
@Service
public class UserService {
// 依赖的组件
private final UserMapper userMapper;
private final RedisTemplate<String, Object> redisTemplate;
// 构造器注入(Spring 自动填充依赖)
@Autowired // 单构造器时可省略
public UserService(UserMapper userMapper, RedisTemplate<String, Object> redisTemplate) {
this.userMapper = userMapper;
this.redisTemplate = redisTemplate;
}
// 业务方法
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
@Service
public class UserService {
// 依赖的组件
private final UserMapper userMapper;
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
// 业务方法
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
2. Setter 注入(Setter Injection)
通过 setter 方法注入依赖,必须加 @Autowired:
@Service
public class UserService {
// 依赖的组件(非final)
private UserMapper userMapper;
private RedisTemplate<String, Object> redisTemplate;
// Setter注入
@Autowired
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Autowired
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
3. 字段注入(Field Injection)
直接在字段上加 @Autowired,最简洁但问题最多:
@Service
public class UserService {
// 直接在字段上注入(无需构造器/setter)
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
}
仨、Spring 官方推荐构造器注入的核心原因
Spring 官方文档(Core Technologies)明确指出:构造器注入是强制依赖的最佳选择 ,核心原因可总结为 6 点,从基础到进阶逐步拆解:
1. 保证依赖不可变(Immutable),符合 "不可变优先" 设计原则
- 构造器注入的依赖可声明为 final 关键字(如上例中 private final UserMapper userMapper),一旦赋值就无法修改;
- Setter / 字段注入的依赖不能用 final,运行时可能被意外篡改(如反射、手动调用 setter),存在线程安全风险;
- 不可变对象是线程安全的,且语义更清晰:"这个依赖是服务类运行的必要条件,创建时必须指定,且不能变更"。
2. 保证依赖非空(NonNull),避免空指针异常
- 构造器注入时,Spring 必须在创建对象时就填充所有依赖,若依赖不存在(如 Bean 未定义),会直接抛出 NoSuchBeanDefinitionException,启动阶段就暴露问题 ;
- Setter / 字段注入:对象创建和依赖注入是两步操作,若依赖缺失,对象创建成功但依赖为 null,运行时调用依赖方法才会抛出空指针,问题暴露延迟(可能上线后才发现)。
示例对比:
- 构造器注入:启动时就报错
No qualifying bean of type 'UserMapper' available,提前发现问题;- 字段注入:启动正常,调用
getUserById时才抛NullPointerException,排查成本高。
3. 代码可测试性更强(脱离 Spring 容器)
单元测试时,构造器注入无需 Spring 容器,直接手动 new 即可注入 Mock 依赖:
// 单元测试(无需Spring上下文)
@Test
public void testGetUserById() {
// 模拟依赖
UserMapper mockMapper = Mockito.mock(UserMapper.class);
RedisTemplate mockRedis = Mockito.mock(RedisTemplate.class);
// 手动构造对象(构造器注入的优势)
UserService userService = new UserService(mockMapper, mockRedis);
// 测试逻辑
User user = userService.getUserById(1L);
Assert.assertNotNull(user);
}
而字段注入必须依赖 Spring Test 上下文(@SpringBootTest)或反射才能注入 Mock,测试代码更复杂、运行更慢:
// 字段注入的测试(依赖Spring)
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@MockBean // 需Spring容器支持
private UserMapper userMapper;
@Test
public void testGetUserById() {
// 测试逻辑
}
}
4. 避免循环依赖问题(提前暴露)
Spring 能解决 Setter / 字段注入的循环依赖,但构造器注入会直接暴露循环依赖问题 ,强制你优化代码设计:
- 循环依赖示例:A 的构造器依赖 B,B 的构造器依赖 A → Spring 启动时直接报错 Circular reference involving bean 'a';
- Setter / 字段注入:Spring 会通过 "三级缓存" 暂时存放未完全初始化的对象,表面上解决了循环依赖,但本质是掩盖了代码设计缺陷(两个类耦合过紧)。
官方认为:循环依赖是代码设计问题,应修复而非 "绕过",构造器注入能倒逼你拆分职责、解耦代码。
5. 符合 "依赖注入" 的本质(显式声明依赖)
依赖注入的核心是 "明确声明组件的依赖",构造器注入让依赖关系可视化、显式化 :
- 看构造器就能清楚知道:这个服务类需要哪些依赖才能运行,语义清晰;
- 字段注入的依赖隐藏在代码中,不看字段定义无法知道依赖关系(尤其类代码量大时)。
6. 兼容更多 Spring 特性,无兼容性问题
- 字段注入依赖 Spring 的 AutowiredAnnotationBeanPostProcessor 后置处理器,若自定义 BeanPostProcessor 顺序错误,可能导致注入失败;
- 构造器注入是 Spring 最基础的注入方式,兼容性最好,不受后置处理器、代理模式(如 CGLIB/JDK 动态代理)的影响。
肆、不同注入方式的适用场景
| 注入方式 | 核心优势 | 适用场景 | 官方态度 |
|---|---|---|---|
| 构造器注入 | 不可变、非空、易测试 | 强制依赖(组件运行的必要条件) | 强烈推荐 |
| Setter 注入 | 可选依赖、运行时可修改 | 可选依赖(如配置类的可选属性) | 推荐(仅可选依赖) |
| 字段注入 | 代码简洁 | 无(仅临时测试 / 非生产代码) | 不推荐 |
最佳实践:
- 强制依赖(如 Service 依赖 Mapper、Repository)→ 构造器注入;
- 可选依赖(如 Service 依赖一个可选的缓存组件)→ Setter 注入(加 @Autowired(required = false));
- 彻底放弃字段注入(即使代码简洁,也不要用在生产代码中)。
五、总结
Spring 官方推荐构造器注入的核心原因可归纳为 3 个关键点:
- 可靠性 :final 保证不可变,构造时填充依赖保证非空,启动阶段暴露问题;
- 可测试性 :脱离 Spring 容器即可手动构造,单元测试更高效;
- 设计合理性 :显式声明依赖、暴露循环依赖问题,倒逼代码解耦。
补充:Spring 4.3+ 后对构造器注入做了优化 ------ 若类只有一个构造器,可省略 @Autowired,进一步简化代码,这也体现了官方对构造器注入的主推态度。
