搞懂@Autowired 与@Resuorce

0 阅读11分钟

在 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。它首先不关心类型匹配与否,先找名字。 例:

@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: 只能用在 类、方法、字段(属性)上,不能用在构造方法上。

总结对比表 image.png

举例说明

假设有一个接口 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 无法使用。
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 少写一行注解)。
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 容器。

构造器注入

```
<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>
@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 = 电池(没有它,车动不了)。 单元测试 = 你(用户)想在家里试玩一下这辆车,看看能不能跑。

  1. 字段注入:像“焊死的电池仓” 假设代码是这样的(字段注入):
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)),暴力塞进去。代码写起来又臭又长,还容易破坏内部结构。

  1. 构造器注入:像“开放的电池槽” 现在代码改成了这样(构造器注入):
    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 的,是官方预留的合法通道。

  1. 什么是 Mock?(为什么不需要真实环境) 你可能注意到代码里写的是 MockUserService。

真实环境 (Spring 启动):UserService 可能会连接数据库、连接Redis。如果在测试时用真的,你还得先配置好数据库,跑一次测试要几分钟。

单元测试 (Mock):我们只测 UserController 的逻辑(比如判断用户是否登录),不关心数据库坏没坏。 所以我们弄个“假人”(Mock 对象),它是一个空的 UserService,不连数据库,只为了让 UserController 能跑起来。

总结:用字段注入:变量被锁在 private 的黑盒子里,不用 Spring 很难把假对象塞进去进行测试。 用构造器注入:类显式地把“缺口”露出来(构造方法),你在测试时可以手动、快速、简单地把假对象传进去,运行速度飞快。

  1. 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),它都能通过构造方法传进去,从而实现轻松测试。