Spring 注入三剑客:@Resource、@Autowired、@RequiredArgsConstructor 到底该用哪个?

分享 AI 与代码,顺便抢救发际线。

大家好,我是 IT空门·门主


一、缘起:一次 Code Review 引发的"血案"

事情是这样的。

前两天给团队做代码 review,一个 20 行的 Service 被我打回了 3 次

不是因为逻辑写错了,而是------

A 同学用 @Autowired

B 同学用 @Resource

C 同学用 @RequiredArgsConstructor

三个人,三种写法,吵了一下午。

最后组长来了一句:"别争了,你们三个都给我写一篇文章讲清楚。"

于是,就有了你正在看的这篇。

这篇文章,我会把 Spring 注入里最常见的三个注解 一次性讲透:

  • 它们是什么怎么来的
  • 它们怎么工作的
  • 它们怎么用什么时候用
  • 它们各有什么坑

看完之后,下次再有同事问你,直接把这篇文章甩过去就行


二、先看一句话结论

怕你没耐心看完,先把结论放在前面:

注解 一句话定位
@Autowired Spring 家的"老大哥",按类型 找,找不到再按名字
@Resource JDK 亲儿子(JSR-250),按名字 找,找不到再按类型
@RequiredArgsConstructor Lombok 出品,构造器注入的语法糖,Spring 官方推荐姿势

至于项目里到底用哪个,先卖个关子,看完你就知道了。

但我可以先剧透:90% 的新项目,都应该用 @RequiredArgsConstructor。


三、三个注解的"出身"

开始之前,先搞清楚这三个家伙到底是谁家的娃

这一点非常关键,因为出身决定了它们的行为

1. @Autowired ------ Spring 亲儿子

  • 出品方:Spring 框架
  • 位置:org.springframework.beans.factory.annotation.Autowired
  • 引入时间:Spring 2.5+
  • 注入方式:字段注入(当然也能用在构造器上,但大家基本不这么写)

2. @Resource ------ JDK 官方嫡子

  • 出品方:JSR-250 规范(Java EE 标准)
  • 位置:jakarta.annotation.Resource
  • 注意:Spring 把它纳入了自家生态
  • 注入方式:字段注入(也能用在 setter 上)

3. @RequiredArgsConstructor ------ Lombok 神器

  • 出品方:Lombok
  • 位置:lombok.RequiredArgsConstructor
  • 本质:是一个编译期生成构造器的注解
  • 注入方式:构造器注入(由 Spring 4.3+ 自动支持)

看到没?三个注解,两个干活的(@Autowired、@Resource),一个造工具的(@RequiredArgsConstructor)

@RequiredArgsConstructor 自己不注入,它造出一个构造器,让 Spring 通过构造器注入。


四、源码级原理:它们到底是怎么注入的?

来,上硬菜。

1. @Autowired 的注入逻辑

@Autowired 默认按 byType(按类型)查找 Bean。

查找顺序如下:

复制代码
1. 先按类型在容器中找
2. 找到唯一一个 → 直接注入
3. 找到多个 → 再按名字匹配
4. 一个都没找到 → 抛 NoSuchBeanDefinitionException

简化版源码逻辑(来自 AutowiredAnnotationBeanPostProcessor):

java 复制代码
// 伪代码,省略异常分支
public void doResolveDependency(...) {
    // 1. 按类型找
    Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type);
    
    // 2. 多个候选,按 @Primary / @Priority 选
    if (matchingBeans.size() > 1) {
        // ... 选择逻辑
    }
    
    // 3. 还不行,按属性名作为 beanName 再找
    if (matchingBeans.isEmpty()) {
        matchingBeans = findAutowireCandidates(beanName + "." + propertyName, type);
    }
}

总结一句话:@Autowired 先类型,后名字


2. @Resource 的注入逻辑

@Resource 默认按 byName(按名字)查找 Bean。

查找顺序如下:

复制代码
1. 先按 name 属性找
2. 找不到 → 按字段名(即属性名)找
3. 还找不到 → 按类型找
4. 找到多个 → 抛 NoUniqueBeanDefinitionException

简化版逻辑(来自 CommonAnnotationBeanPostProcessor):

java 复制代码
// 伪代码
protected Object getResource(LookupElement element, String requestingBeanName) {
    // 1. 优先按 name
    if (StringUtils.hasLength(element.name)) {
        return resourceFactory.getBean(element.name);
    }
    
    // 2. 按字段名
    // 3. 兜底按类型
    // ...
}

总结一句话:@Resource 先名字,后类型

这就是它和 @Autowired 最本质的区别


3. @RequiredArgsConstructor 的注入逻辑

这个稍微有点不一样。

@RequiredArgsConstructor 本身不参与注入,它的工作流程是:

复制代码
1. Lombok 在编译期生成一个包含所有 final / @NonNull 字段的构造器
2. Spring 在创建 Bean 时,发现有构造器 → 自动通过构造器注入
3. 注入逻辑:按类型找(本质和 @Autowired 一致)

举个例子:

java 复制代码
@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserMapper userMapper;  // final 字段
    
    private String commonName;            // 普通字段,不会被注入
}

编译后,Lombok 会生成:

java 复制代码
@Service
public class UserService {
    
    private final UserMapper userMapper;
    private String commonName;
    
    // Lombok 自动生成 ↓
    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
}

注意:只有 final 字段和 @NonNull 字段才会被纳入构造器。

这也是为什么 Lombok 让你必须把字段声明为 final


五、对比表格:一张表看懂三剑客

废话不多说,直接上图:

维度 @Autowired @Resource @RequiredArgsConstructor
出身 Spring JDK(JSR-250) Lombok
默认注入方式 byType byName 构造器注入
可作用位置 字段、构造器、方法、参数 字段、setter 类(编译期生成构造器)
是否需要 getter/setter
是否能注入 final 字段 ❌ 不建议 ❌ 不建议 ✅ 必须 final
能否指定 Bean 名字 @Qualifier name 属性 @Qualifier 配合
是否支持 @Primary
是否支持 Optional 注入 ✅ (@Autowired(required=false))
能否脱离 Spring 使用 ✅(纯 JDK) ✅(只是普通构造器)
字段能否被修改 能(普通字段) 能(普通字段) ❌(final,不可变)
是否便于单元测试 ❌(需要 Spring 容器) ✅(直接 new 即可)
Spring 官方推荐 ⚠️ 旧推荐 ⚠️ 不推荐 现在首推

六、实战代码:三种写法长什么样?

为了让你有更直观的感受,我用同一个 Service 写三遍。

需求:UserService 需要 UserMapper 和 RedisTemplate。

写法 1:@Autowired(字段注入)

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
}

这是最经典、也是最被吐槽的写法。

看着简洁,其实埋了一堆雷,后面避坑部分会说


写法 2:@Resource(字段注入)

java 复制代码
@Service
public class UserService {

    @Resource
    private UserMapper userMapper;

    @Resource(name = "redisTemplate")
    private RedisTemplate<String, Object> redisTemplate;
}

@Resource 也能省略 name 属性 ,此时按字段名找。

注意 @Resource(name = "redisTemplate"),这个 name 就是 Bean 的名字。

如果你注入了 RedisTemplate<Object, Object>那就得加上 type 或者换名字,否则会冲突。


写法 3:@RequiredArgsConstructor(构造器注入)

java 复制代码
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;
    private final RedisTemplate<String, Object> redisTemplate;
}

干净、简洁、不可变。

编译后等价于显式写一个构造器

java 复制代码
@Service
public class UserService {

    private final UserMapper userMapper;
    private final RedisTemplate<String, Object> redisTemplate;

    // 等价于 ↓
    public UserService(UserMapper userMapper, 
                       RedisTemplate<String, Object> redisTemplate) {
        this.userMapper = userMapper;
        this.redisTemplate = redisTemplate;
    }
}

七、项目中到底用哪个?为什么?

重点来了。

结论先行

新项目 / 新代码:统一使用 @RequiredArgsConstructor(构造器注入)

⚠️ 老项目维护:保持原样即可,不要全量重构

新代码不要再用 @Autowired 字段注入

为什么是构造器注入?

Spring 官方在 spring-framework-reference反复强调

Always use constructor-based dependency injection in your beans.

始终在你的 Bean 中使用基于构造器的依赖注入

为什么?我给你列 5 条无法反驳的理由

理由 1:依赖不可变(final 字段)
java 复制代码
private final UserMapper userMapper;

final 关键字让字段只能赋值一次 ,注入后再也无法被修改

这意味着:不会有"被偷偷换掉"的风险

字段注入就不一样了,理论上你可以在某个奇怪的地方 userMapper = null,然后整个服务就炸了。


理由 2:依赖不为空(编译期保证)

构造器注入:

java 复制代码
public UserService(UserMapper userMapper) {
    this.userMapper = userMapper;  // 必须传,否则编译/启动报错
}

字段注入:

java 复制代码
@Autowired
private UserMapper userMapper;  // 万一没注入,运行时才 NPE

字段注入不会在编译期告诉你有问题

你以为一切正常,启动后某个冷门调用直接给你抛 NullPointerException。

Bug 没解决,但 CPU 温度先上来了。


理由 3:便于单元测试

构造器注入:

java 复制代码
// 直接 new 就行
UserService userService = new UserService(mockMapper);

字段注入:

java 复制代码
// 麻烦得一逼
UserService userService = new UserService();
userService.setUserMapper(mockMapper);          // 需要 setter
// 或者用反射
ReflectionTestUtils.setField(userService, "userMapper", mockMapper);

一个字:

两个字:很烦

三个字:写测试

四个字:队友跑路


理由 4:避免循环依赖

字段注入和 setter 注入天然支持循环依赖(Spring 会用三级缓存帮你兜底)。

这听起来是优点?

不,这是坑

因为循环依赖本身就是设计问题,Spring 帮你解决,只是把炸弹埋得更深。

构造器注入会在启动时直接抛出 BeanCurrentlyInCreationException强制你解决循环依赖

不要靠 Spring 帮你擦屁股。

架构干净,比什么都重要。


理由 5:代码可读性

字段注入的代码:

java 复制代码
@Service
public class UserService {
    @Autowired private A a;
    @Autowired private B b;
    @Autowired private C c;
    @Autowired private D d;
}

一眼看过去,只知道"这里有一堆字段",不知道哪些是依赖、哪些是状态

构造器注入的代码:

java 复制代码
@Service
@RequiredArgsConstructor
public class UserService {
    private final A a;
    private final B b;
    private final C c;
    private final D d;
}

final 字段就是依赖 ,普通字段就是状态

一目了然,强迫症患者的福音


八、避坑指南:我踩过的那些坑

这部分是付费内容(开玩笑的)。

但真的,这 8 个坑,每一个都能让你 debug 到天亮


坑 1:@Autowired 注入多个同类型 Bean 不指定名字

java 复制代码
public interface MessageSender {
    void send(String msg);
}

@Service
public class SmsSender implements MessageSender { ... }

@Service
public class EmailSender implements MessageSender { ... }

@Service
public class NotificationService {
    @Autowired
    private MessageSender messageSender;  // 💥 启动报错
}

报错

text 复制代码
NoUniqueBeanDefinitionException: 
No qualifying bean of type 'MessageSender' available: 
expected single matching bean but found 2: emailSender,smsSender

解决方案 A:用 @Primary 标记主 Bean

java 复制代码
@Service
@Primary
public class SmsSender implements MessageSender { ... }

解决方案 B:用 @Qualifier 指定

java 复制代码
@Autowired
@Qualifier("smsSender")
private MessageSender messageSender;

解决方案 C:用 @RequiredArgsConstructor + @Qualifier

java 复制代码
@Service
@RequiredArgsConstructor
public class NotificationService {
    @Qualifier("smsSender")
    private final MessageSender messageSender;
}

坑 2:@Resource 按名字找不到时不会报错?

错!@Resource 找不到 bean 也会报错

java 复制代码
@Resource
private UserMapper userMapperXxx;  // 名字写错
text 复制代码
NoSuchBeanDefinitionException: 
No bean named 'userMapperXxx' available

唯一不同的是:@Autowired 找不到还能 fallback 到按类型找

@Resource 直接抛异常,更严格。


坑 3:@RequiredArgsConstructor 必须加 final

java 复制代码
@Service
@RequiredArgsConstructor
public class UserService {
    private UserMapper userMapper;  // ⚠️ 没有 final
}

这字段不会被 Lombok 加进构造器

结果就是:userMapper 永远是 null

启动不报错,调用时 NPE。最阴险的 bug 之一。


坑 4:构造器注入导致循环依赖

java 复制代码
@Service
public class AService {
    private final BService bService;
    public AService(BService bService) { this.bService = bService; }
}

@Service
public class BService {
    private final AService aService;
    public BService(AService aService) { this.aService = aService; }
}

报错

text 复制代码
BeanCurrentlyInCreationException: 
Error creating bean with name 'aService': 
Requested bean is currently in creation: 
Is there an unresolvable circular reference?

解决方案

  1. 重构代码:把公共逻辑抽到第三个 Service(CService)
  2. 改用 @Lazy(不推荐,只是缓兵之计)
java 复制代码
public AService(@Lazy BService bService) { this.bService = bService; }
  1. 改成 setter 注入(最不推荐,但 Spring 允许)

坑 5:@Autowired 字段注入 + final

java 复制代码
@Autowired
private final UserMapper userMapper;  // 💥 编译错误

Java 不允许 final 字段在声明时不赋值。

字段注入又不能像构造器那样在构造时赋值。

所以 final 字段只能用构造器注入


坑 6:Optional 依赖

有时候一个 Bean 可能存在也可能不存在

java 复制代码
@Autowired(required = false)
private OptionalBean optionalBean;

@Resource@RequiredArgsConstructor 都不直接支持这种 Optional 注入。

如果你非要用构造器,可以这样:

java 复制代码
public UserService(Optional<OptionalBean> optionalBean) {
    this.optionalBean = optionalBean.orElse(null);
}

坑 7:和 @Value 一起用

@Value 注入配置:

java 复制代码
@Value("${app.timeout:3000}")
private int timeout;

字段注入 + @Value 没问题。

构造器注入 + @Value 需要这样写:

java 复制代码
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserMapper userMapper;
    private final int timeout;
    
    // Lombok 生成的构造器不会处理 @Value
    // 需要手写或用 @ConfigurationProperties
}

此时推荐用 @ConfigurationProperties 替代一堆 @Value。


坑 8:测试时如何绕过 final

@RequiredArgsConstructor 生成的 final 字段,Mockito 默认不能 mock

错,Mockito 5+ 已经支持 final 类/方法了。

如果你用的版本较老:

java 复制代码
// 在 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
mock-maker-inline

九、一些"老程序员"才知道的小细节

这些点面试爱考,但日常开发经常被忽略。

1. @Autowired 和 @Resource 在 Setter 上的区别

java 复制代码
// @Autowired 用在 setter
@Autowired
public void setUserMapper(UserMapper userMapper) {
    this.userMapper = userMapper;
}

// @Resource 用在 setter
@Resource
public void setUserMapper(UserMapper userMapper) {
    this.userMapper = userMapper;
}

行为基本一致,都支持 byName + byType。

实际项目里基本没人这么写,了解即可。


2. @Autowired 的 required 属性

java 复制代码
@Autowired(required = false)
private UserMapper userMapper;

表示:找不到 Bean 也不报错,注入 null

这是 @Resource 和 @RequiredArgsConstructor 做不到的事


3. @Resource 不会注入 Spring 自定义 Bean?

错。@Resource 完全可以用在 Spring 管理的 Bean 上

只是因为它是 JDK 标准,在脱离 Spring 的环境也能用(比如纯 Java EE)。


4. Spring 6 / Spring Boot 3 的变化

  • javax.annotationjakarta.annotation
  • @Resource 的包名变了,但用法不变
  • spring-boot-starter 默认不包含 jakarta.annotation-api,需要手动加
xml 复制代码
<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
</dependency>

十、最终选型指南(一图流)

场景 推荐方案
新项目 / 新模块 @RequiredArgsConstructor
Spring 官方推荐 ✅ 构造器注入
老项目(已经在用 @Autowired) ⚠️ 保持原样,新增代码用构造器
需要 Optional 注入 @Autowired(required=false)Optional<T> 构造器参数
需要脱离 Spring 容器 @Resource(JDK 标准)
多实现 Bean 注入 @Qualifier + 构造器注入
配置注入 @ConfigurationProperties,别用 @Value 一把梭

结语

技术这个东西,用哪个都能跑

"能跑" 和 "跑得好" 是两码事

写代码这件事:

能用 → 可读 → 可维护 → 可演进

构造器注入,刚好是后三者的最优解。

下次再有人问你 "到底用哪个",把今天这篇文章甩他脸上


技术不难,难的是没人告诉你坑在哪。

如果这篇帮到你,点个 ,顺便帮我抢救一下发际线 😄

我是 IT空门·门主,我们下期见。


🙏 作者介绍

📌 写文不易,Bug 更不易。

如果这篇文章对你有帮助,可以搜一搜:空门技术栈

这里分享:

  • ✅ Java / Spring AI / 企业级项目实战
  • ✅ Docker / RAG知识库 / 微服务踩坑
  • ✅ Python、前端、AI应用落地
  • ✅ 偶尔分享一些「头发保卫战」经验 😆

一个热爱技术、持续填坑的开发者,

陪你一起少踩坑,少加班,多写优雅代码。

📖 推荐阅读


🤝 技术交流 / 项目合作

平时也会做一些技术项目与咨询,包括:

  • Java / Spring Boot 企业级项目开发
  • AI 应用开发(LangChain、RAG、Agent、知识库)
  • Docker / Linux / 私有化部署
  • 系统功能开发、接口对接、性能优化
  • 疑难问题排查与技术咨询

如果你:

  • 想做 AI 项目,但不确定技术方案
  • 项目卡在某个 Bug 很久
  • 想把 AI 接入现有系统
  • 需要企业级开发支持

欢迎交流。

📮 联系方式:

  • Email:2929119150@qq.com
  • 也可以私信我
  • 技术交流可通过个人主页联系

有些坑,一个人踩是事故;一起踩,就是经验 😎

相关推荐
ServBay1 小时前
云端 AI 蜜月期宣告结束,为什么 2026 年开发者转向本地优先架构
后端·ai编程
IT_陈寒1 小时前
Vite这个坑我帮你踩了,动态导入居然这样才生效
前端·人工智能·后端
Sam_Deep_Thinking1 小时前
Spring Boot 的启动原理是什么?
java·spring boot·后端
南部余额2 小时前
Spring WebClient 从入门到精通
java·后端·spring
CodeStats2 小时前
从 CPU 指令到 JVM 进程:彻底讲透 Java 执行 main 方法时,类加载、主线程、栈帧入栈的完整底层逻辑
java·linux·开发语言
摇滚侠2 小时前
Spring 零基础入门到进阶 基于注解管理 Bean 38-43
xml·java·后端·spring·intellij-idea
SamDeepThinking2 小时前
我们当年是如何真实落地BFF的?
java·后端·架构
码语智行2 小时前
Shapefile获取空间数据和中心点坐标
java·arcgis