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 团队通常提倡构造方法注入。

原因是构造方法注入能保证:

  1. 依赖不可变 —— final 关键字
  2. 依赖不为空 —— 构造时检查
  3. 完全初始化 —— 对象创建完就能用,不会出现半初始化状态
  4. 方便测试 —— 不需要 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 的方式进行依赖注入。它虽然不是代码量最少的,但却是最规范、最安全、最好测试的方式。

一个成熟的项目,不只需要"能跑就行"的代码,更需要"经得起推敲"的代码。从依赖注入这个细节做起,你的代码质量会上一个台阶。


以上就是本期分享,如果你觉得有帮助,欢迎点赞收藏。下期再见!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐