Java Spring

@Autowired, @Resource, @Inject 三个注解的区别

Spring 支持使用@Autowired, @Resource, @Inject 三个注解进行依赖注入。下面来介绍一下这三个注解有什么区别。

@Autowired

@Autowired为Spring 框架提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired
这里先给出一个示例代码,方便讲解说明:

  1. public interface Svc {
  2. void sayHello();
  3. }
  4. @Service
  5. public class SvcA implements Svc {
  6. @Override
  7. public void sayHello() {
  8. System.out.println("hello, this is service A");
  9. }
  10. }
  11. @Service
  12. public class SvcB implements Svc {
  13. @Override
  14. public void sayHello() {
  15. System.out.println("hello, this is service B");
  16. }
  17. }
  18. @Service
  19. public class SvcC implements Svc {
  20. @Override
  21. public void sayHello() {
  22. System.out.println("hello, this is service C");
  23. }
  24. }

测试类:

  1. @SpringBootTest
  2. public class SimpleTest {
  3. @Autowired
  4. // @Qualifier("svcA")
  5. Svc svc;
  6. @Test
  7. void rc() {
  8. Assertions.assertNotNull(svc);
  9. svc.sayHello();
  10. }
  11. }

装配顺序:

  1. 按照type在上下文中查找匹配的bean

    查找type为Svc的bean

  2. 如果有多个bean,则按照name进行匹配

  3. 如果有@Qualifier注解,则按照@Qualifier指定的name进行匹配

    查找name为svcA的bean

  4. 如果没有,则按照变量名进行匹配

    查找name为svc的bean

  5. 匹配不到,则报错。(@Autowired(required=false),如果设置requiredfalse(默认为true),则注入失败时不会抛出异常)

    @Inject

    在Spring 的环境下,@Inject@Autowired 是相同的 ,因为它们的依赖注入都是使用AutowiredAnnotationBeanPostProcessor来处理的。
    2021-07-25-18-58-10-130526.png
    @Inject是 JSR-330 定义的规范 ,如果使用这种方式,切换到Guice也是可以的。
    Guice 是 google 开源的轻量级 DI 框架
    如果硬要说两个的区别,首先@Inject是Java EE包里的,在SE环境需要单独引入。另一个区别在于@Autowired可以设置required=false@Inject并没有这个属性。

    @Resource

    @Resource是JSR-250定义的注解。Spring 在 CommonAnnotationBeanPostProcessor实现了对JSR-250的注解的处理,其中就包括@Resource
    2021-07-25-18-58-10-269525.png
    @Resource有两个重要的属性:nametype,而Spring 将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。
    装配顺序:

  6. 如果同时指定了nametype,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。

  7. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。
  8. 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。
  9. 如果既没有指定name,又没有指定type,则默认按照byName方式进行装配;如果没有匹配,按照byType进行装配。

    IDEA 提示 Field injection is not recommended

    在使用IDEA 进行Spring 开发的时候,在字段上面使用@Autowired注解的时候,IDEA 会有警告提示:

    Field injection is not recommended Inspection info: Spring Team Recommends: “Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies”.

2021-07-25-18-58-10-366524.png
翻译过来就是这个意思:

不建议使用基于 field 的注入方式。 Spring 开发团队建议:在Spring Bean 永远使用基于constructor 的方式进行依赖注入。对于必须的依赖,永远使用断言来确认。

比如如下代码:

  1. @Service
  2. public class HelpService {
  3. @Autowired
  4. @Qualifier("svcB")
  5. private Svc svc;
  6. public void sayHello() {
  7. svc.sayHello();
  8. }
  9. }
  10. public interface Svc {
  11. void sayHello();
  12. }
  13. @Service
  14. public class SvcB implements Svc {
  15. @Override
  16. public void sayHello() {
  17. System.out.println("hello, this is service B");
  18. }
  19. }

将光标放到@Autowired处,使用Alt + Enter 快捷进行修改之后,代码就会变成基于Constructor的注入方式,修改之后 :

  1. @Service
  2. public class HelpService {
  3. private final Svc svc;
  4. @Autowired
  5. public HelpService(@Qualifier("svcB") Svc svc) {
  6. // Assert.notNull(svc, "svc must not be null");
  7. this.svc = svc;
  8. }
  9. public void sayHello() {
  10. svc.sayHello();
  11. }
  12. }

如果按照Spring 团队的建议,如果svc是必须的依赖,应该使用Assert.notNull(svc, "svc must not be null")来确认。
修正这个警告提示固然简单,但是更重要是去理解为什么Spring 团队会提出这样的建议?直接使用这种基于 field 的注入方式有什么问题?


首先需要知道,Spring 中有这么3种依赖注入的方式 :

  • 基于 field 注入(属性注入)
  • 基于 setter 注入
  • 基于 constructor 注入(构造器注入)

    1. 基于 field 注入

    所谓基于 field 注入,就是在bean的变量上使用注解进行依赖注入。本质上是通过反射的方式直接注入到field,所以private的成员也可以被注入具体的对象。这是平常开发中看的最多也是最熟悉的一种方式,同时,也正是 Spring 团队所不推荐的方式。比如:
    1. @Autowired
    2. private Svc svc;

    2. 基于 setter 方法注入

    通过对应变量的setXXX()方法以及在方法上面使用注解,来完成依赖注入。比如:
    1. private Helper helper;
    2. @Autowired
    3. public void setHelper(Helper helper) {
    4. this.helper = helper;
    5. }
    注:在 Spring 4.3 及以后的版本中,setter 上面的 @Autowired 注解是可以不写的。

    3. 基于 constructor 注入

    将各个必需的依赖全部放在带有注解构造方法的参数中,并在构造方法中完成对应变量的初始化,这种方式,就是基于构造方法的注入,也是日常最为推荐的一种使用方式。这种注入方式很直接,通过对象构建的时候建立关系,所以这种方式对对象创建的顺序会有要求,当然Spring会搞定这样的先后顺序,除非出现循环依赖,然后就会抛出异常。比如: ```java private final Svc svc;

@Autowired public HelpService(@Qualifier(“svcB”) Svc svc) { this.svc = svc; } `` 在 Spring 4.3 及以后的版本中,如果这个类只有一个构造方法,那么这个构造方法上面也可以不写@Autowired` 注解。

三种依赖注入的对比

在知道了Spring提供的三种依赖注入方式之后,继续回到开头说到的问题:IDEA为什么不推荐使用Field Injection呢?
可以从多个开发测试的考察角度来对比一下它们之间的优劣:

可靠性

从对象构建过程和使用过程,看对象在各阶段的使用是否可靠来评判:

  • Field Injection:不可靠
  • Constructor Injection:可靠
  • Setter Injection:不可靠

由于构造函数有严格的构建顺序和不可变性,一旦构建就可用,且不会被更改。

可维护性

主要从更容易阅读、分析依赖关系的角度来评判:

  • Field Injection:差
  • Constructor Injection:好
  • Setter Injection:差

还是由于依赖关键的明确,从构造函数中可以显现的分析出依赖关系,对于如何去读懂关系和维护关系更友好。

可测试性

当在复杂依赖关系的情况下,考察程序是否更容易编写单元测试来评判

  • Field Injection:差
  • Constructor Injection:好
  • Setter Injection:好

Constructor Injection和Setter Injection的方式更容易Mock和注入对象,所以更容易实现单元测试。

灵活性

主要根据开发实现时候的编码灵活性来判断:

  • Field Injection:很灵活
  • Constructor Injection:不灵活
  • Setter Injection:很灵活

由于Constructor Injection对Bean的依赖关系设计有严格的顺序要求,所以这种注入方式不太灵活。相反Field Injection和Setter Injection就非常灵活,但也因为灵活带来了局面的混乱,也是一把双刃剑。

循环关系的检测

对于Bean之间是否存在循环依赖关系的检测能力:

  • Field Injection:不检测
  • Constructor Injection:自动检测
  • Setter Injection:不检测

    性能表现

    不同的注入方式,对性能的影响

  • Field Injection:启动快

  • Constructor Injection:启动慢
  • Setter Injection:启动快

主要影响就是启动时间,由于Constructor Injection有严格的顺序要求,所以会拉长启动时间。
所以,综合上面各方面的比较,可以获得如下表格:
Spring为什么建议构造器注入 - 图4
结果一目了然,Constructor Injection在很多方面都是由于其他两种方式的,所以Constructor Injection通常都是首选方案!
而Setter Injection比起Field Injection来说,大部分都一样,但因为可测试性更好,所以当要用@Autowired的时候,推荐使用Setter Injection的方式,这样IDEA也不会给出警告了。同时,也侧面也反映了,可测试性的重要地位!

基于 field 注入的好处

这种方式非常的简洁,代码看起来很简单,通俗易懂。类可以专注于业务而不被依赖注入所污染。只需要把@Autowired扔到变量之上就好了,不需要特殊的构造器或者set方法,依赖注入容器会提供所需的依赖。

基于 field 注入的坏处

基于 field 注入虽然简单,但是却会引发很多的问题。这些问题在平常开发阅读项目代码的时候就经常遇见。

  • 容易违背了单一职责原则 使用这种基于 field 注入的方式,添加依赖是很简单的,就算类中有十几个依赖可能都觉得没有什么问题,普通的开发者很可能会无意识地给一个类添加很多的依赖。但是当使用构造器方式注入,到了某个特定的点,构造器中的参数变得太多以至于很明显地发现something is wrong。拥有太多的依赖通常意味着类要承担更多的责任,明显违背了单一职责原则(SRP:Single responsibility principle)。
  • 依赖注入与容器本身耦合依赖注入框架的核心思想之一就是受容器管理的类不应该去依赖容器所使用的依赖。换句话说,这个类应该是一个简单的POJO(Plain Ordinary Java Object)能够被单独实例化并且也能为它提供它所需的依赖。这个问题具体可以表现在:
    • 类和依赖容器强耦合,不能在容器外使用
    • 类不能绕过反射(例如单元测试的时候)进行实例化,必须通过依赖容器才能实例化,这更像是集成测试
  • 不能使用属性注入的方式构建不可变对象(final 修饰的变量)

    Spring 开发团队的建议

    Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies.

简单来说,就是

  • 强制依赖就用构造器方式
  • 可选、可变的依赖就用setter 注入当然可以在同一个类中使用这两种方法。构造器注入更适合强制性的注入旨在不变性,Setter注入更适合可变性的注入。

看看Spring 这样推荐的理由,首先是基于构造方法注入,

The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore constructor-injected components are always returned to client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.

Spring 团队提倡使用基于构造方法的注入,因为这样一方面可以将依赖注入到一个不可变的变量中 (注:final 修饰的变量) ,另一方面也可以保证这些变量的值不会是 null 。此外,经过构造方法完成依赖注入的组件 (注:比如各个 service),在被调用时可以保证它们都完全准备好了 。与此同时,从代码质量的角度来看,一个巨大的构造方法通常代表着出现了代码异味,这个类可能承担了过多的责任 。
而对于基于 setter 的注入,他们是这么说的:

Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later.

基于 setter 的注入,则只应该被用于注入非必需的依赖,同时在类中应该对这个依赖提供一个合理的默认值。如果使用 setter 注入必需的依赖,那么将会有过多的 null 检查充斥在代码中。使用 setter 注入的一个优点是,这个依赖可以很方便的被改变或者重新注入 。

总结

最后给出两个结论:

  1. 依赖注入的使用上,Constructor Injection是首选。
  2. 使用@Autowired注解的时候,要使用Setter Injection方式,这样代码更容易编写单元测试。