在 Java(通常指 Spring 框架环境下)开发中,@Autowired 和 @Resource 都可以用来实现依赖注入(Dependency Injection),将 Spring 容器中的 Bean 注入到变量中。
尽管它们的功能相似,但在来源、注入策略和支持范围上存在明显的区别。以下是详细对比:
1. 来源不同 (Origin)
@Autowired: 属于 Spring 框架 (org.springframework.beans.factory.annotation.Autowired)。 如果你不再使用 Spring 框架,该注解将失效。
@Resource: 属于 Java 标准 (JSR-250 规范)。 旧版本包名:javax.annotation.Resource。 新版本(JDK 9+ 或 Spring Boot 3+):jakarta.annotation.Resource。 因为它属于 Java 标准,所以即便更换了容器框架(如从 Spring 换到 EJB),理论上代码改动更小(虽然在实际业务开发中很少换框架)。
2. 默认注入策略不同 (Injection Strategy) ------ 核心区别
@Autowired:
默认 按类型装配 (byType)。Spring 会在容器中寻找与属性类型匹配的 Bean。
-
"类型"指的是变量的 Java 类(Class)或接口(Interface)。它定义了这个对象"是什么"(它的基因、它的功能模版)。
-
在代码中: 就是修饰符(private/public)后面、变量名(fieldName)前面的那个词。 Spring 的逻辑 (byType): 当 Spring 容器进行 byType (按类型) 装配时,它不关心你给变量起了什么名字(叫 user 还是 u 还是 abc 都无所谓),它只关心容器里有没有也是这个类(或接口的实现类)的 Bean 特例:如果有多个相同类型的 Bean(例如一个接口有多个实现类),Spring 无法决定注入哪一个,此时会报错。 解决方案:通常配合 @Qualifier("beanName") 注解使用,强制指定 Bean 的名称。
@Resource:
默认 按名称装配 (byName)。 它首先会提取属性的变量名(field name),作为 Bean id 在容器中寻找。 回退机制:如果按名称找不到,它会尝试 按类型 (byType) 去寻找。 注意:一旦指定了 name 或 type 属性,回退机制就会失效,必须严格匹配。
-
"名称"指的是 Bean 在 Spring 容器中的唯一 ID。 它定义了这个对象"具体是哪一个"(类似于身份证号)。默认情况下,Spring Bean 的名称就是类名首字母小写(例如类 UserDao 的默认 Bean 名字是 userDao)。
-
在代码中: 默认情况下,Spring 会把你的变量名(Field Name)当作要查找的 Bean 名称。 Spring 的逻辑 (byName): 当 Spring 容器进行 byName (按名称) 装配时,它主要看你的变量名叫什么,或者注解里指定的 name 是什么,然后去容器里找 ID 完全匹配的 Bean。它首先不关心类型匹配与否,先找名字。 例:
less
@Component("dog") // 显式指定 Bean 名称为 "dog"
public class Dog implements Animal {}
@Component("cat") // 显式指定 Bean 名称为 "cat"
public class Cat implements Animal {}
// 注入代码:
@Resource
private Animal cat;
这里的 变量名 (Name) 是:cat。
这里的 类型 (Type) 是:Animal。
byName 逻辑:@Resource 默认先看名字。它会去容器里找:"有没有一个 Bean 的 ID 叫 cat ?" 结果:找到了!虽然 Dog 也符合 Animal 类型,但因为名字不匹配,所以 Spring 准确地注入了 Cat 对象。
3. 参数与属性不同
@Autowired:
只有一个属性:required。 默认为 true,表示被注入的 Bean 必须存在,否则抛出异常。 如果允许为 null,可以设置 @Autowired(required = false)。 @Resource:
有两个重要属性:name 和 type。 @Resource(name = "myBean"):强制只按名称查找。 @Resource(type = User.class):强制只按类型查找。
4. 作用范围不同
@Autowired: 可以用在 构造方法、方法、字段(属性)、参数上。 @Resource: 只能用在 类、方法、字段(属性)上,不能用在构造方法上。
总结对比表 
举例说明
假设有一个接口 UserService 和两个实现类 UserServiceImplA 和 UserServiceImplB。
场景 1:使用 @Autowired
//
@Autowired
private UserService userService;
// 正确示范:配合 @Qualifier
@Autowired
@Qualifier("userServiceImplA")
private UserService userService;
场景 2:使用 @Resource
//
// 只要变量名写成 userServiceImplA,就会自动找到对应的 Bean
@Resource
private UserService userServiceImplA;
// 方式 2:显式指定 name
@Resource(name = "userServiceImplB")
private UserService userService; // 变量名随便写,以 name 属性为准
5. 实践建议
- 构造器注入(推荐): 现在 Spring 官方推荐使用构造器注入(结合 final 关键字和 Lombok 的 @RequiredArgsConstructor),这种情况下只能隐式使用 Spring 的逻辑(类似 @Autowired),此时 @Resource 无法使用。
@RestController
public class UserController {
// 1. 变量可以(也建议)声明为 final,表示一旦赋值不可修改
private final UserService userService;
// 2. 提供一个构造方法,参数就是你需要注入的 Bean
// 注意:Spring 4.3+ 以后,如果类只有一个构造方法,@Autowired 可以省略不写
// @Autowired
public UserController(UserService userService) {
this.userService = userService;
}
public void sayHello() {
userService.doSomething();
}
}
- 字段注入: 如果你的 Bean 只有唯一的实现类,使用 @Autowired 或 @Resource 都可以。 如果已知某个接口有多个实现类,且你需要指定具体的一个,使用 @Resource(name="...") 代码会更简洁(比 @Autowired + @Qualifier 少写一行注解)。
@RestController
public class UserController {
// 直接在字段上加注解,Spring 通过反射暴力注入
@Autowired
private UserService userService;
public void sayHello() {
userService.doSomething();
}
}
- 为什么官方推荐构造器注入?(核心好处)
你可能会觉得:"字段注入只要一行代码,构造器注入还要写个方法,好麻烦!" 但构造器注入有以下优势:
① 保证依赖不可变 (Immutability)
代码体现:字段可以用 final 修饰(如上例 private final UserService ...)。
好处:一旦对象被创建,这个依赖就定死了,不会被意外修改,线程更安全。而在字段注入中,你不能加 final,因为 Java 规定 final 变量必须在构造完成前赋值,而字段注入是在对象创建之后才注入的。
② 保证对象不为空 (Null Safety)
逻辑:构造方法是创建对象的必经之路。 好处:如果你使用构造器注入,Spring 在创建 UserController 时,如果找不到 UserService,程序在启动时就会直接报错(崩溃) 。
对比:如果是字段注入,Spring 可能因为某些配置错误没注入进去(依然是 null),你启动时不报错,等到用户真正调用 sayHello() 方法时报 NullPointerException,导致生产事故。构造器注入能把问题暴露在启动阶段。
③ 方便写单元测试 (Testability)
场景 :你要测试 UserController 的逻辑,不想启动整个笨重的 Spring 容器。
构造器注入:
scss
```
<JAVA>
// 纯 Java 代码,不需要 Spring 环境
UserService mockService = new MockUserService(); // 创建个假的 Service
UserController controller = new UserController(mockService); // 直接 new 出来
controller.sayHello();
```
字段注入 :因为字段是 private 的,除了反射你没办法给它赋值。你被迫要启动 Spring 容器或者使用复杂的测试框架来注入 Mock 对象,测试变得很麻烦。这是目前 Java 后端开发中最优雅、最主流的写法。
- 构造器注入配合 Lombok
虽然构造器注入好,但手写构造方法确实累(如果依赖了 10 个 Service,构造方法参数巨长)。 Lombok 插件完美解决了这个问题。
使用 @RequiredArgsConstructor 注解:
java
<JAVA>
@RestController
@RequiredArgsConstructor // 1. Lombok 自动生成包含所有 final 字段的构造方法
public class UserController {
// 2. 只要声明为 final 即可,不需要写 @Autowired,也不用手写构造器
private final UserService userService;
private final OrderService orderService;
private final PayService payService;
public void test() {
userService.doSomething();
}
}
6. 补充理解:
5.③ 方便写单元测试 (Testability) - 场景 :你要测试 UserController 的逻辑,不想启动整个笨重的 Spring 容器。
- 构造器注入:
> UserService mockService = new MockUserService(); // 创建个假的 Service UserController
controller = new UserController(mockService); // 直接 new 出来 controller.sayHello();
- 字段注入 :因为字段是
private的,除了反射你没办法给它赋值。你被迫要启动 Spring 容器或者使用复杂的测试框架来注入 Mock 对象,测试变得很麻烦。
需要先弄清楚 "单元测试"的目标 和 Java 访问权限(private)的限制 之间的冲突。
用一个形象的比喻:给遥控汽车装电池。
UserController = 遥控汽车。 UserService = 电池(没有它,车动不了)。 单元测试 = 你(用户)想在家里试玩一下这辆车,看看能不能跑。
- 字段注入:像"焊死的电池仓" 假设代码是这样的(字段注入):
>public class UserController {
// ⚠️ 注意:这是 private 的!而且没有 set 方法,也没有构造方法传参
@Autowired
private UserService userService;
public void run() {
userService.doWork(); // 没电池会报空指针
}
}
现在你想测试(试玩):
你拿起车:UserController car = new UserController(); ------ 成功。
你想装电池:你手里有一个假电池(MockUserService),你想把它塞进去。
问题来了:你看不到电池仓(它是 private 的)。 你也没有螺丝刀拆卸它(没有构造方法,没有 Setter 方法)。 尴尬局面:你手里拿着电池,但死活塞不进车里。车子一跑就坏(NullPointerException)。 怎么解决?(为什么说麻烦) 你必须动用"黑科技":
方法A(启动 Spring):把整个汽车修理厂搬到你家客厅(启动 Spring 容器)。Spring 拥有特权,能穿墙把电池放进去。但这太慢了!
方法B(Java 反射):你强行把车壳砸开(使用反射代码 field.setAccessible(true)),暴力塞进去。代码写起来又臭又长,还容易破坏内部结构。
- 构造器注入:像"开放的电池槽" 现在代码改成了这样(构造器注入):
public
private final UserService userService;
// 官方提供的入口:想造这辆车,必须给我电池
public UserController(UserService userService) {
this.userService = userService;
}
public void run() {
userService.doWork();
}
}
现在你再测试:
你想玩车:你必须遵循规则 new UserController(...)。
规则要求:构造方法强制要求你手里必须拿着电池才能造车。
你装电池:
>UserService fakeBattery = new MockUserService(); // 假电池
UserController car = new UserController(fakeBattery); // 直接从构造口塞进去!
结果:车造好了,电池也在里面了,直接跑! 为什么说方便?
不需要修理厂(Spring):你是在用最普通的 Java 语法(new 对象),没有任何框架依赖。 不需要砸车(反射):因为构造方法是 public 的,是官方预留的合法通道。
- 什么是 Mock?(为什么不需要真实环境) 你可能注意到代码里写的是 MockUserService。
真实环境 (Spring 启动):UserService 可能会连接数据库、连接Redis。如果在测试时用真的,你还得先配置好数据库,跑一次测试要几分钟。
单元测试 (Mock):我们只测 UserController 的逻辑(比如判断用户是否登录),不关心数据库坏没坏。 所以我们弄个"假人"(Mock 对象),它是一个空的 UserService,不连数据库,只为了让 UserController 能跑起来。
总结:用字段注入:变量被锁在 private 的黑盒子里,不用 Spring 很难把假对象塞进去进行测试。 用构造器注入:类显式地把"缺口"露出来(构造方法),你在测试时可以手动、快速、简单地把假对象传进去,运行速度飞快。
- MockUserService是什么样的?
通常 UserService 是一个接口 (Interface)。如果是类同理继承该类。
MockUserService 其实就是你自己写的一个"假的"实现类。它的作用是冒充真的 UserService,但是它不干实事(不连数据库、不发网络请求),只返回假数据。
这里有三种方式来实现它,由浅入深:
- 方式一:手写一个类(最直观的理解) 这是最原始的方式。为了测试,你专门写了一个类来实现接口。
假设:真正的接口定义
>public interface UserService {
String getUserName(Integer id);
}
定义的假对象 (MockUserService.java)
<JAVA>
// 这是一个普通的 Java 类,只用于测试环境
public class MockUserService implements UserService {
@Override
public String getUserName(Integer id) {
// 真正的 Service 会去查数据库: return userMapper.selectById(id).getName();
// 假的 Service 直接返回固定值,骗过调用者
System.out.println("我是假冒的,我没有连数据库!");
return "张三(测试版)";
}
}
测试代码:
>// 1. 创建假的
UserService mockService = new MockUserService();
// 2. 注入
UserController controller = new UserController(mockService);
// 3. 运行
controller.test(); // 输出:张三(测试版)
- 方式二:匿名内部类(偷懒写法) 如果你不想专门新建一个 MockUserService.java 文件,你可以在测试代码里现场定义它:
>public void test() {
// 现场 new 一个接口的实现
UserService mockService = new UserService() {
@Override
public String getUserName(Integer id) {
return "我是现场编造的假数据";
}
};
UserController controller = new UserController(mockService);
controller.sayHello();
}
原理:这也是纯 Java 语法,不需要 Spring。
方式三:使用 Mockito 框架(工作中的标准写法) 在实际工作中,如果有 50 个方法,手写 MockUserService 去实现这 50 个方法太累了。 我们会使用 Mockito 库,它能通过动态代理自动生成这个假对象。
>// 1. 让 Mockito 帮我们生成一个假的 Service
// 这一行代码在内存里动态生成了一个对象,效果等同于上面的 new MockUserService()
UserService mockService = Mockito.mock(UserService.class);
// 2. 告诉这个假对象:如果有人调你,你怎么演戏
// "如果有人调 getUserName(1),你就返回 '李四'"
Mockito.when(mockService.getUserName(1)).thenReturn("李四");
// 3. 通过构造器注入进去
UserController controller = new UserController(mockService);
// 4. 测试
controller.sayHello();
MockUserService 的定义本质上就是: 一个实现了 UserService 接口,但里面全是"假动作"的 Java 类。 构造器注入的优势在于:因为它接受的是 UserService 接口类型,所以无论是"真的 Service"(Spring 里的 Bean)还是"假的 Service"(你手写的 Mock),它都能通过构造方法传进去,从而实现轻松测试。