文章目录
- 前言
-
- [一、先看最核心区别:一个偏 Java 标准,一个偏 Spring 体系](#一、先看最核心区别:一个偏 Java 标准,一个偏 Spring 体系)
-
- [1.1 `@Autowired`:默认按照类型注入](#1.1
@Autowired:默认按照类型注入) - [1.2 `@Resource`:默认按照名称注入](#1.2
@Resource:默认按照名称注入) - [1.3 两者最核心的区别](#1.3 两者最核心的区别)
- [1.1 `@Autowired`:默认按照类型注入](#1.1
- 二、字段注入的问题:为什么更推荐构造方法注入?
-
- [2.1 字段注入会隐藏类的真实依赖](#2.1 字段注入会隐藏类的真实依赖)
- [2.2 字段注入不利于单元测试](#2.2 字段注入不利于单元测试)
- [2.3 字段注入不方便使用 `final`](#2.3 字段注入不方便使用
final) - [2.4 字段注入容易掩盖循环依赖问题](#2.4 字段注入容易掩盖循环依赖问题)
- [2.5 实际开发更推荐构造方法注入](#2.5 实际开发更推荐构造方法注入)
- 写在文后
🔥 个人主页:铁皮哥(欢迎关注)
📌 作者简介:28届校招生,后端开发/Agent 方向在学
📚 学习内容:Java、Python、计算机视觉、大语言模型、Agent开发
📝 专栏内容:从零开始的Claude Code零代码生活(持续更新中)
✨不只背八股,更想搞懂为什么这样设计
前言
这篇文章准备聊一个 Spring 里特别常见、但又很容易被一句话带过的知识点:@Resource 和 @Autowired 的区别。
很多同学刚学 Spring 的时候,写依赖注入基本都是照着项目里的代码来。看到别人用 @Autowired,自己也跟着用;看到有的类里写的是 @Resource,也觉得反正都能把 Bean 注入进来,好像没什么区别。
但真到面试或者实际排查问题时,这个知识点就没那么简单了。
比如:
java
@Autowired
private PayService payService;
和:
java
@Resource
private PayService payService;
这两段代码看起来效果差不多,但它们背后的匹配规则并不一样。尤其是当一个接口有多个实现类时,如果没搞清楚"按类型注入"和"按名称注入"的区别,就很容易遇到 Bean 注入失败、启动报错,或者注入结果和自己预期不一致的问题。
不过,这篇文章不会只停留在"@Autowired 按类型,@Resource 按名称"这种结论上。
更重要的是,很多项目里常见的这种字段注入写法:
java
@Autowired
private UserService userService;
虽然写起来很方便,但它并不是最推荐的依赖注入方式。字段注入会让类的依赖关系变得不够直观,也不利于单元测试和不可变设计。
一、先看最核心区别:一个偏 Java 标准,一个偏 Spring 体系
@Autowired 是 Spring 提供的注解,而 @Resource 来自 Java 规范。
不过在日常 Spring 项目里,我们更关心的不是它们来自哪里,而是它们到底按照什么规则找 Bean。
1.1 @Autowired:默认按照类型注入
先看一个最常见的写法:
java
@Service
public class OrderService {
@Autowired
private UserService userService;
}
这段代码的意思是:Spring 容器启动后,会去容器里找一个 UserService 类型的 Bean,然后注入到 userService 这个字段里。
所以 @Autowired 的核心规则是:默认按照类型匹配。
比如容器里有这样一个 Bean:
java
@Service
public class UserService {
}
那么 Spring 发现 OrderService 需要一个 UserService,容器里刚好也有一个 UserService 类型的对象,就可以正常注入。
这个场景很好理解,因为只有一个类型匹配的 Bean,Spring 不需要纠结。
但如果容器里有多个同类型 Bean,情况就不一样了。比如一个接口有多个实现类:
java
public interface PayService {
void pay();
}
java
@Service
public class AliPayService implements PayService {
@Override
public void pay() {
System.out.println("支付宝支付");
}
}
java
@Service
public class WeChatPayService implements PayService {
@Override
public void pay() {
System.out.println("微信支付");
}
}
这时候再这样写:
java
@Autowired
private PayService payService;
Spring 会发现容器里有两个 PayService 类型的 Bean:
text
aliPayService
weChatPayService
它就不知道到底应该注入哪一个了。
这也是很多人第一次遇到依赖注入报错的地方:不是没有 Bean,而是符合条件的 Bean 太多了。
这种情况下,一般需要配合 @Qualifier 指定 Bean 名称:
java
@Autowired
@Qualifier("aliPayService")
private PayService payService;
说清楚我要注入的是名字叫 aliPayService 的那个 Bean,此时可以正确注入所需要的 Bean 对象。
1.2 @Resource:默认按照名称注入
再看 @Resource。
java
@Service
public class OrderService {
@Resource
private UserService userService;
}
这段代码看起来和 @Autowired 差不多,但它的匹配思路不一样。
@Resource 默认会先根据字段名去找 Bean。也就是说,它会先拿字段名 userService 去容器里找有没有同名的 Bean。
如果有,就直接注入。
比如:
java
@Service
public class UserService {
}
默认情况下,这个 Bean 在 Spring 容器里的名字通常就是:
text
userService
所以 @Resource 可以正常注入。
如果想写得更明确,也可以直接指定 Bean 名称:
java
@Resource(name = "userService")
private UserService userService;
再来看刚才多个实现类的例子。
java
public interface PayService {
void pay();
}
有两个实现类:
java
@Service
public class AliPayService implements PayService {
}
java
@Service
public class WeChatPayService implements PayService {
}
如果我们这样写:
java
@Resource
private PayService aliPayService;
@Resource 会优先根据字段名 aliPayService 去找 Bean。容器里刚好有一个名字叫 aliPayService 的 Bean,所以可以注入成功。
如果字段名不想和 Bean 名称保持一致,也可以显式指定:
java
@Resource(name = "weChatPayService")
private PayService payService;
1.3 两者最核心的区别
整理一下,@Autowired 和 @Resource 最大的区别其实就两个:来源不同,匹配规则不同。
可以先记住这张对比表:
| 对比项 | @Autowired |
@Resource |
|---|---|---|
| 来源 | Spring 提供 | Java 规范提供 |
| 默认匹配方式 | 按类型匹配 | 按名称匹配 |
| 多个实现类时 | 通常配合 @Qualifier |
可以通过字段名或 name 指定 |
| 常见写法 | @Autowired + @Qualifier |
@Resource(name = "...") |
所以,面试时如果被问到它们的区别,不要只答一句:
@Autowired按类型,@Resource按名称。
这样太干了,也容易显得只是背结论。
可以换成更完整的说法:
@Autowired是 Spring 提供的注解,默认按照类型注入。当容器里存在多个相同类型的 Bean 时,需要配合@Qualifier指定名称。
@Resource是 Java 规范提供的注解,默认优先按照名称注入,可以通过name属性指定 Bean 名称。如果名称匹配不到,再按类型进行匹配。
二、字段注入的问题:为什么更推荐构造方法注入?
前面聊的是 @Resource 和 @Autowired 的区别,它们解决的是"Spring 按什么规则把 Bean 注入进来"的问题。
但在实际开发里,还有一个更值得关注的问题:我们到底应该用什么方式完成依赖注入?
很多项目里都能看到这种写法:
java
@Service
public class OrderService {
@Autowired
private PayService payService;
}
或者:
java
@Service
public class OrderService {
@Resource
private PayService payService;
}
这种写法叫字段注入,也就是直接把依赖注入到成员变量上。
它的优点很明显:写起来简单,代码也短。刚开始写 Spring 项目时,会觉得这种方式非常顺手。
但问题也正是出在这里:它太方便了,方便到我们很容易忽略类本身的依赖关系。
2.1 字段注入会隐藏类的真实依赖
比如下面这个类:
java
@Service
public class OrderService {
@Resource
private PayService payService;
@Resource
private UserService userService;
@Resource
private CouponService couponService;
@Resource
private StockService stockService;
}
只看类名 OrderService,我们很难第一时间知道它到底依赖了多少东西。
依赖全部散落在字段上,如果类越来越大,字段越来越多,这个类的复杂度就会被慢慢藏起来。
而构造方法注入会把依赖关系直接暴露出来:
java
@Service
public class OrderService {
private final PayService payService;
private final UserService userService;
private final CouponService couponService;
private final StockService stockService;
public OrderService(PayService payService,
UserService userService,
CouponService couponService,
StockService stockService) {
this.payService = payService;
this.userService = userService;
this.couponService = couponService;
this.stockService = stockService;
}
}
虽然代码看起来变长了一点,但好处也很明显:这个类需要哪些依赖,一眼就能看到。
如果构造方法里的参数越来越多,甚至多到看着有点难受,那它反而是在提醒我们:
这个类可能承担了太多职责,是不是该考虑拆分了?
字段注入则没有这种提醒,它会让类在不知不觉中变胖。
2.2 字段注入不利于单元测试
字段注入还有一个很常见的问题:脱离 Spring 容器后,对象不好创建。
比如:
java
@Service
public class OrderService {
@Resource
private PayService payService;
public void createOrder() {
payService.pay();
}
}
如果在单元测试里直接这样写:
java
OrderService orderService = new OrderService();
orderService.createOrder();
这段代码很可能会出现空指针异常。
因为 payService 是 Spring 通过反射注入的,而我们手动 new OrderService() 的时候,并没有经过 Spring 容器,所以这个字段不会被自动赋值。
当然,我们也可以用反射或者测试框架去强行塞值,但这会让测试代码变得不够自然。
如果使用构造方法注入,测试就直接很多:
java
PayService payService = new MockPayService();
OrderService orderService = new OrderService(payService);
需要什么依赖,就在创建对象时传进去。
这样写更符合普通 Java 对象的使用方式,也不会让这个类过度依赖 Spring 容器。
2.3 字段注入不方便使用 final
字段注入通常不能很好地配合 final 使用。
比如下面这种写法是不合适的:
java
@Resource
private final PayService payService;
因为 final 字段要求在对象构造完成时就完成初始化,而字段注入是在对象创建之后,再由 Spring 通过反射给字段赋值。
这两个时机是不一致的。
而构造方法注入天然适合配合 final:
java
@Service
public class OrderService {
private final PayService payService;
public OrderService(PayService payService) {
this.payService = payService;
}
}
这样写的好处是:payService 一旦在构造方法里被赋值,后面就不能被随便修改。
对于 Service 这类组件来说,依赖关系通常应该是稳定的。对象创建好之后,它依赖哪些组件,一般不应该再变化。
所以构造方法注入不仅是注入方式的变化,也是在让代码更接近不可变设计。
2.4 字段注入容易掩盖循环依赖问题
字段注入还有一个很容易被忽略的问题:它可能会让循环依赖看起来"没问题"。
比如有两个 Service 互相依赖:
java
@Service
public class AService {
@Resource
private BService bService;
}
java
@Service
public class BService {
@Resource
private AService aService;
}
这就是一个典型的循环依赖:
text
AService 依赖 BService
BService 又依赖 AService
在某些情况下,项目可能还能正常启动。
原因不是这种设计没问题,而是 Spring 在创建 Bean 的过程中做了一些处理。简单来说,Spring 为了解决部分循环依赖问题,引入了多级缓存机制,会提前暴露还没有完全初始化完成的 Bean 引用。
也就是说,Spring 可能会先把一个"半成品对象"提前放出来,让另一个 Bean 先拿到它,从而让整个创建流程继续走下去。
项目能启动,并不代表代码设计是合理的。
从业务角度看,两个 Service 互相依赖,很多时候说明职责边界已经不清楚了。可能是 AService 做了 BService 该做的事,也可能是 BService 又反过来依赖了不该依赖的逻辑。
字段注入的问题就在于,它会让这种循环依赖不够明显。
因为依赖关系都藏在字段里,代码写着写着,很容易变成:
text
A 调 B
B 调 C
C 又调 A
最后项目看起来还能跑,但调用链已经绕成了一团。
这类问题在小项目里可能不明显,一旦业务变复杂,排查起来会非常痛苦。
更重要的是,不要把"Spring 能处理一部分循环依赖"理解成"循环依赖是正确写法"。
这更像是框架在帮我们兜底,而不是鼓励我们这样设计。
如果使用构造方法注入,循环依赖会更早暴露出来。
比如:
java
@Service
public class AService {
private final BService bService;
public AService(BService bService) {
this.bService = bService;
}
}
java
@Service
public class BService {
private final AService aService;
public BService(AService aService) {
this.aService = aService;
}
}
这种写法下,AService 创建时需要 BService,BService 创建时又需要 AService,依赖关系会在启动阶段就暴露得很明显。
这其实是好事。
因为循环依赖大多数时候不是"注入方式问题",而是"代码设计问题"。构造方法注入能更早提醒我们:这里的类关系可能需要重新设计。
常见的处理方式有几种:
text
把公共逻辑抽到第三个 Service
重新划分两个 Service 的职责边界
通过事件发布解耦
让上层流程编排调用两个 Service,而不是让它们互相调用
不要为了让项目启动成功,就把循环依赖藏起来。
能在启动阶段暴露的问题,往往比上线后才暴露的问题更便宜。
2.5 实际开发更推荐构造方法注入
所以,比起字段注入,更推荐的写法是构造方法注入:
java
@Service
public class OrderService {
private final PayService payService;
public OrderService(PayService payService) {
this.payService = payService;
}
}
如果项目里使用 Lombok,还可以进一步简化:
java
@Service
@RequiredArgsConstructor
public class OrderService {
private final PayService payService;
}
@RequiredArgsConstructor 会为 final 字段生成构造方法。这样既保留了构造方法注入的优点,代码也不会显得太啰嗦。
写在文后
期待您的一键三连!如果有什么问题或建议欢迎在评论区交流!