分享 AI 与代码,顺便抢救发际线。
大家好,我是 IT空门·门主。
一、缘起:一次 Code Review 引发的"血案"
事情是这样的。
前两天给团队做代码 review,一个 20 行的 Service 被我打回了 3 次。
不是因为逻辑写错了,而是------
A 同学用
@AutowiredB 同学用
@ResourceC 同学用
@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?
解决方案:
- 重构代码:把公共逻辑抽到第三个 Service(CService)
- 改用 @Lazy(不推荐,只是缓兵之计)
java
public AService(@Lazy BService bService) { this.bService = bService; }
- 改成 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.annotation→jakarta.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应用落地
- ✅ 偶尔分享一些「头发保卫战」经验 😆
一个热爱技术、持续填坑的开发者,
陪你一起少踩坑,少加班,多写优雅代码。
📖 推荐阅读
- Spring Boot 中最容易被忽略的 5 个性能坑
- Spring AI 接入 DeepSeek 全过程:附 7 个踩坑实录
- AI 为什么总"失忆"?LangChain Memory 完全指南:从 InMemory 到 Redis 实战避坑
- Java 单例模式详解:7 种实现方式 + volatile 原理 + 反射与序列化问题
- 告别手动复制接口文档!Apifox MCP + AI 自动测试让开发效率起飞
- 别再纠结学哪个了!Java AI 三大框架深度对比:Spring AI vs AgentScope-Java
- MySQL MCP Server 从零安装到使用实战,AI 直接查询数据库
🤝 技术交流 / 项目合作
平时也会做一些技术项目与咨询,包括:
- Java / Spring Boot 企业级项目开发
- AI 应用开发(LangChain、RAG、Agent、知识库)
- Docker / Linux / 私有化部署
- 系统功能开发、接口对接、性能优化
- 疑难问题排查与技术咨询
如果你:
- 想做 AI 项目,但不确定技术方案
- 项目卡在某个 Bug 很久
- 想把 AI 接入现有系统
- 需要企业级开发支持
欢迎交流。
📮 联系方式:
- Email:
2929119150@qq.com - 也可以私信我
- 技术交流可通过个人主页联系
有些坑,一个人踩是事故;一起踩,就是经验 😎