搞懂@Autowired 与@Resuorce

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

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

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

相关推荐
大阿明6 小时前
Spring Boot(快速上手)
java·spring boot·后端
哆啦A梦15886 小时前
Springboot整合MyBatis实现数据库操作
数据库·spring boot·mybatis
bearpping6 小时前
Java进阶,时间与日期,包装类,正则表达式
java
邵奈一6 小时前
清明纪念·时光信笺——项目运行指南
java·实战·项目
sunwenjian8867 小时前
Java进阶——IO 流
java·开发语言·python
sinat_255487817 小时前
读者、作家 Java集合学习笔记
java·笔记·学习
墨香幽梦客7 小时前
API集成技术规范:RESTful与GraphQL在企业系统对接中的应用对比
后端·restful·graphql
皮皮林5517 小时前
如何画出一张优秀的架构图?(老鸟必备)
java
百锦再7 小时前
Java 并发编程进阶,从线程池、锁、AQS 到并发容器与性能调优全解析
java·开发语言·jvm·spring·kafka·tomcat·maven
森林猿7 小时前
java-modbus-读取-modbus4j
java·网络·python