Spring 依赖注入三种方式深度对比:为什么官方推荐构造方法注入?
Spring 依赖注入三种方式深度对比:为什么官方推荐构造方法注入?
前言
最近在重构项目代码时,发现很多人对 Spring 的依赖注入方式选择比较随意。有的用 @Autowired,有的用 @Resource,还有的用 @RequiredArgsConstructor + final。
这三种方式到底有什么区别?为什么 Spring 官方文档强烈推荐构造方法注入?今天用一篇文章讲清楚。
一、先看代码——三种方式长什么样
假设我们有一个 LearningRecordController,需要注入 ILearningRecordService:
方式一:@RequiredArgsConstructor + final ✅
@RestController
@RequiredArgsConstructor // Lombok 自动生成构造方法
public class LearningRecordController {
private final ILearningRecordService learningRecordService;
@GetMapping("/course/{courseId}")
public LearningLessonDTO queryLearningRecordByCourse(@PathVariable Long courseId){
return learningRecordService.queryLearningRecordByCourse(courseId);
}
}
@RequiredArgsConstructor 是 Lombok 提供的注解,它会为类中所有 final 字段生成一个全参构造方法。上面的代码编译后等价于:
public LearningRecordController(ILearningRecordService learningRecordService) {
this.learningRecordService = learningRecordService;
}
方式二:@Autowired 字段注入
@RestController
public class LearningRecordController {
@Autowired
private ILearningRecordService learningRecordService;
// ... 业务方法
}
方式三:@Resource 字段注入
@RestController
public class LearningRecordController {
@Resource
private ILearningRecordService learningRecordService;
// ... 业务方法
}
二、形象类比——餐厅招聘服务员
为了帮助你直观理解这三种方式的区别,我们用一个生活中的场景来类比:
背景:你开了一家餐厅,需要一个服务员来帮忙。
| 注入方式 | 对应现实 | 具体画面 |
|---|---|---|
| 构造方法注入 | 开业前签劳务合同 | 合同白纸黑字写清楚:张三,负责传菜。人岗绑定,不能随便换。 |
@Autowired |
开业后贴"谁有空谁来" | 张三也行,李四也行,王五也行,今天谁来算谁。 |
@Resource |
点名要"王五" | 贴个告示:只找王五来干活。结果王五今天请假了,餐厅就没人干活了。 |
构造方法注入就是最正规的那种——人还没上岗,合同就先签好了,这个人必须到位,出了问题也知道找谁。
三、逐一分析优缺点
1️⃣ @RequiredArgsConstructor + final(构造方法注入)
优点
① 不可变性——锁死了,不能换
private final ILearningRecordService learningRecordService;
final 关键字意味着这个字段一旦赋值,终生不能修改。这在业务上非常重要——你的 controller 依赖的 service 应该是固定的,不应该被运行时偷偷换掉。
② 空安全——编译时就保证不空
因为构造方法强制要求传入参数,Spring 在创建 Bean 时如果找不到对应的依赖,启动阶段就会报错,而不是等到你调用某个方法时才炸出 NullPointerException。
// 启动时直接报错,不会留到运行时
Error: Unsatisfied dependency expressed through constructor parameter 0
③ 单元测试极其方便——直接 new 就行
这是构造方法注入最大的好处之一。写单元测试时不需要启动 Spring 容器,直接像普通 Java 对象一样创建:
// 爽!一行搞定
ILearningRecordService mockService = mock(ILearningRecordService.class);
LearningRecordController ctrl = new LearningRecordController(mockService);
对比一下,如果用的是 @Autowired 字段注入,想写单元测试就得这样:
// 要么启动整个 Spring 容器,耗时几十秒
// 要么用反射强行塞值:
LearningRecordController ctrl = new LearningRecordController();
Field field = ctrl.getClass().getDeclaredField("learningRecordService");
field.setAccessible(true);
field.set(ctrl, mockService);
哪种更优雅,一目了然。
④ 依赖一目了然——看一眼类就知道它依赖什么
@RequiredArgsConstructor
public class LearningRecordController {
private final ILearningRecordService learningRecordService;
private final IUserService userService;
private final ICourseService courseService;
// 扫一眼这三个 final 字段,就知道这个 controller 依赖了三个 service
}
相比之下,@Autowired 的类你得一个个数注解,才看全它的依赖清单。
缺点
没有任何实质性的缺点。硬要说的话,就是多了一个 Lombok 的依赖,并且类上多了一个 @RequiredArgsConstructor 注解。但这点代价和它带来的好处相比,不值一提。
2️⃣ @Autowired 字段注入
优点
就一个:写起来最省事。一行 @Autowired 搞定,不用写构造方法,不用加 Lombok 注解。
缺点
① 单元测试困难
前面已经说过了,想 mock 依赖必须用反射或者启动 Spring 容器。
② 容易产生 NPE
如果 Spring 容器里没有 ILearningRecordService 这个 Bean,@Autowired 字段不会被赋值,而是保持 null。编译时完全检查不出来,只有运行时调用了这个字段才会报错。
@RestController
public class LearningRecordController {
@Autowired
private ILearningRecordService learningRecordService; // 如果注入失败,这里是 null
public void doSomething() {
// 如果上面注入失败了,这里直接 NPE
learningRecordService.query(); // NullPointerException!
}
}
③ 违反了"单一职责"的直觉
从代码规范的角度看,一个类的依赖应该通过构造方法明确"要"进来,而不是运行时通过反射"塞"进去。@Autowired 字段注入依赖了 Spring 的反射机制来赋值,这不符合常规的 Java 编程思维。
3️⃣ @Resource 字段注入
优点
Java 原生注解,不依赖 Spring 框架。如果你把项目从 Spring 迁移到其他框架(比如 Guice),@Resource 仍然可以用。
缺点
① 按名称匹配,容易注入错误
@Resource 的查找逻辑是:先按名字找,找不到再按类型找。这就会带来隐患:
@Resource
private ILearningRecordService learningRecordService;
如果 Spring 容器里有一个 Bean 名字也叫 learningRecordService(但类型不是 ILearningRecordService),就会注入错误的对象。
而 @Autowired 是按类型匹配的,类型不对直接报错,更加安全。
② 不支持 Spring 的高级特性
@Autowired 支持 required = false(允许注入为 null)、@Qualifier(指定具体 Bean 名称)等功能,@Resource 都不支持。
四、Spring 官方怎么说?
Spring 官方文档中明确提到:
“The Spring team generally advocates constructor injection…”
Spring 团队通常提倡构造方法注入。
原因是构造方法注入能保证:
- 依赖不可变 ——
final关键字 - 依赖不为空 —— 构造时检查
- 完全初始化 —— 对象创建完就能用,不会出现半初始化状态
- 方便测试 —— 不需要 Spring 容器也能测
而且从 Spring 4.3 开始,如果类只有一个构造方法,连 @Autowired 注解都可以省略,Spring 会自动调用:
@RestController
public class LearningRecordController {
private final ILearningRecordService learningRecordService;
// 只有一个构造方法,Spring 4.3+ 自动注入,连 @Autowired 都不用写
public LearningRecordController(ILearningRecordService learningRecordService) {
this.learningRecordService = learningRecordService;
}
}
Lombok 的 @RequiredArgsConstructor 就是自动帮你生成这个构造方法,省去手动写的麻烦。
五、总结对比表
| 对比维度 | final + 构造方法 ✅ |
@Autowired |
@Resource |
|---|---|---|---|
| 代码简洁性 | ⭐⭐⭐ 尚可 | ⭐⭐⭐⭐⭐ 最简洁 | ⭐⭐⭐⭐ 简洁 |
| 不可变安全 | ✅ final 锁死 |
❌ 可被反射修改 | ❌ 可被反射修改 |
| 空安全 | ✅ 编译时保证 | ❌ 运行时可能 NPE | ❌ 运行时可能 NPE |
| 单元测试 | ✅ 直接 new | ❌ 需要反射/容器 | ❌ 需要反射/容器 |
| 依赖可见性 | ✅ 一目了然 | ❌ 需要数注解 | ❌ 需要数注解 |
| Spring 推荐度 | ⭐⭐⭐⭐⭐ 最推荐 | ⭐⭐⭐ 可用 | ⭐⭐ 不推荐 |
写在最后
在实际项目开发中,建议统一使用 @RequiredArgsConstructor + final 的方式进行依赖注入。它虽然不是代码量最少的,但却是最规范、最安全、最好测试的方式。
一个成熟的项目,不只需要"能跑就行"的代码,更需要"经得起推敲"的代码。从依赖注入这个细节做起,你的代码质量会上一个台阶。
以上就是本期分享,如果你觉得有帮助,欢迎点赞收藏。下期再见!
更多推荐



所有评论(0)