1. 概述

在 Spring 框架教程中,我们将演示如何使用与依赖注入相关的注解,即 @Resource@Inject@Autowired。这些注解为类提供了声明式依赖解析方式:

@Autowired 
ArbitraryClass arbObject;

与直接实例化(命令式方式)形成对比:

ArbitraryClass arbObject = new ArbitraryClass();

三个注解中有两个属于 Java 扩展包:javax.annotation.Resourcejavax.inject.Inject@Autowired 注解则属于 org.springframework.beans.factory.annotation 包。

每个注解都支持字段注入和 setter 注入两种依赖解析方式。我们将通过一个简化但实用的示例,基于每个注解的执行路径,演示三者的区别。

示例将重点展示在集成测试中如何使用这三种注入注解。测试所需的依赖可以是任意文件或任意类。

2. @Resource 注解

@Resource 注解属于 JSR-250 注解集合,随 Jakarta EE 一起提供。该注解按以下优先级顺序执行:

  1. 按名称匹配
  2. 按类型匹配
  3. 按限定符匹配

这些执行路径同时适用于 setter 注入和字段注入。

2.1. 字段注入

通过在实例变量上添加 @Resource 注解实现字段注入。

2.1.1. 按名称匹配

以下集成测试演示按名称匹配的字段注入:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestResourceNameType.class)
public class FieldResourceInjectionIntegrationTest {

    @Resource(name="namedFile")
    private File defaultFile;

    @Test
    public void givenResourceAnnotation_WhenOnField_ThenDependencyValid(){
        assertNotNull(defaultFile);
        assertEquals("namedFile.txt", defaultFile.getName());
    }
}

代码解析:在 FieldResourceInjectionTest 集成测试的第 7 行,我们通过将 bean 名称作为属性值传递给 @Resource 注解,实现了按名称解析依赖:

@Resource(name="namedFile")
private File defaultFile;

此配置将使用按名称匹配的执行路径解析依赖。必须在 ApplicationContextTestResourceNameType 应用上下文中定义 namedFile bean。

注意:bean ID 和对应的引用属性值必须匹配:

@Configuration
public class ApplicationContextTestResourceNameType {

    @Bean(name="namedFile")
    public File namedFile() {
        File namedFile = new File("namedFile.txt");
        return namedFile;
    }
}

如果未在应用上下文中定义 bean,将抛出 org.springframework.beans.factory.NoSuchBeanDefinitionException。我们可以通过修改 ApplicationContextTestResourceNameType@Bean 的属性值,或修改 FieldResourceInjectionTest@Resource 的属性值来验证这一点。

2.1.2. 按类型匹配

要演示按类型匹配的执行路径,只需移除 FieldResourceInjectionTest 集成测试第 7 行的属性值:

@Resource
private File defaultFile;

然后重新运行测试。

测试仍会通过,因为当 @Resource 注解未接收 bean 名称作为属性值时,Spring 框架会进入下一优先级——按类型匹配——来尝试解析依赖。

2.1.3. 按限定符匹配

要演示按限定符匹配的执行路径,需修改集成测试场景:在 ApplicationContextTestResourceQualifier 应用上下文中定义两个 bean:

@Configuration
public class ApplicationContextTestResourceQualifier {

    @Bean(name="defaultFile")
    public File defaultFile() {
        File defaultFile = new File("defaultFile.txt");
        return defaultFile;
    }

    @Bean(name="namedFile")
    public File namedFile() {
        File namedFile = new File("namedFile.txt");
        return namedFile;
    }
}

使用 QualifierResourceInjectionTest 集成测试演示按限定符匹配的依赖解析。此场景中,需要将特定 bean 依赖注入到每个引用变量:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestResourceQualifier.class)
public class QualifierResourceInjectionIntegrationTest {

    @Resource
    private File dependency1;
    
    @Resource
    private File dependency2;

    @Test
    public void givenResourceAnnotation_WhenField_ThenDependency1Valid(){
        assertNotNull(dependency1);
        assertEquals("defaultFile.txt", dependency1.getName());
    }

    @Test
    public void givenResourceQualifier_WhenField_ThenDependency2Valid(){
        assertNotNull(dependency2);
        assertEquals("namedFile.txt", dependency2.getName());
    }
}

运行集成测试时,会抛出 org.springframework.beans.factory.NoUniqueBeanDefinitionException。这是因为应用上下文找到了两个 File 类型的 bean 定义,无法确定应使用哪个 bean 解析依赖。

要解决此问题,需修改 QualifierResourceInjectionTest 集成测试的第 7-10 行:

@Resource
private File dependency1;

@Resource
private File dependency2;

添加以下代码:

@Qualifier("defaultFile")

@Qualifier("namedFile")

使代码块变为:

@Resource
@Qualifier("defaultFile")
private File dependency1;

@Resource
@Qualifier("namedFile")
private File dependency2;

重新运行集成测试,测试将通过。我们的测试证明:即使应用上下文中定义了多个 bean,也可以使用 @Qualifier 注解消除歧义,允许将特定依赖注入到类中。

2.2. Setter 注入

字段注入的执行路径同样适用于基于 setter 的注入。

2.2.1. 按名称匹配

唯一区别是 MethodResourceInjectionTest 集成测试包含 setter 方法:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestResourceNameType.class)
public class MethodResourceInjectionIntegrationTest {

    private File defaultFile;

    @Resource(name="namedFile")
    protected void setDefaultFile(File defaultFile) {
        this.defaultFile = defaultFile;
    }

    @Test
    public void givenResourceAnnotation_WhenSetter_ThenDependencyValid(){
        assertNotNull(defaultFile);
        assertEquals("namedFile.txt", defaultFile.getName());
    }
}

通过注解引用变量对应的 setter 方法实现 setter 注入。然后将 bean 依赖名称作为属性值传递给 @Resource 注解:

private File defaultFile;

@Resource(name="namedFile")
protected void setDefaultFile(File defaultFile) {
    this.defaultFile = defaultFile;
}

本示例复用 namedFile bean 依赖。bean 名称和对应属性值必须匹配。

运行集成测试时,测试将通过。

为验证按名称匹配的执行路径确实解析了依赖,需将传递给 @Resource 注解的属性值更改为自定义值并重新运行测试。这次测试将因 NoSuchBeanDefinitionException 而失败。

2.2.2. 按类型匹配

使用 MethodByTypeResourceTest 集成测试演示基于 setter 的按类型匹配:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestResourceNameType.class)
public class MethodByTypeResourceIntegrationTest {

    private File defaultFile;

    @Resource
    protected void setDefaultFile(File defaultFile) {
        this.defaultFile = defaultFile;
    }

    @Test
    public void givenResourceAnnotation_WhenSetter_ThenValidDependency(){
        assertNotNull(defaultFile);
        assertEquals("namedFile.txt", defaultFile.getName());
    }
}

运行此测试时,测试将通过。

为验证按类型匹配的执行路径确实解析了 File 依赖,需将 defaultFile 变量的类类型更改为其他类型(如 String)。然后重新执行 MethodByTypeResourceTest 集成测试,这次将抛出 NoSuchBeanDefinitionException

该异常验证了按类型匹配确实用于解析 File 依赖。NoSuchBeanDefinitionException 表明引用变量名称无需与 bean 名称匹配,依赖解析取决于 bean 的类类型与引用变量的类类型是否匹配。

2.2.3. 按限定符匹配

使用 MethodByQualifierResourceTest 集成测试演示按限定符匹配的执行路径:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestResourceQualifier.class)
public class MethodByQualifierResourceIntegrationTest {

    private File arbDependency;
    private File anotherArbDependency;

    @Test
    public void givenResourceQualifier_WhenSetter_ThenValidDependencies(){
      assertNotNull(arbDependency);
        assertEquals("namedFile.txt", arbDependency.getName());
        assertNotNull(anotherArbDependency);
        assertEquals("defaultFile.txt", anotherArbDependency.getName());
    }

    @Resource
    @Qualifier("namedFile")
    public void setArbDependency(File arbDependency) {
        this.arbDependency = arbDependency;
    }

    @Resource
    @Qualifier("defaultFile")
    public void setAnotherArbDependency(File anotherArbDependency) {
        this.anotherArbDependency = anotherArbDependency;
    }
}

我们的测试证明:即使应用上下文中定义了特定类型的多个 bean 实现,也可以结合使用 @Qualifier@Resource 注解解析依赖。

与基于字段的依赖注入类似,如果应用上下文中定义了多个 bean,必须使用 @Qualifier 注解指定用于解析依赖的 bean,否则将抛出 NoUniqueBeanDefinitionException

3. @Inject 注解

@Inject 注解属于 JSR-330 注解集合。该注解按以下优先级顺序执行:

  1. 按类型匹配
  2. 按限定符匹配
  3. 按名称匹配

这些执行路径同时适用于 setter 注入和字段注入。要使用 @Inject 注解,需将 javax.inject 库声明为 Gradle 或 Maven 依赖。

Gradle 配置:

testCompile group: 'javax.inject', name: 'javax.inject', version: '1'

Maven 配置:

<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>

3.1. 字段注入

3.1.1. 按类型匹配

修改集成测试示例使用另一种依赖类型——ArbitraryDependency 类。该类仅作为简单依赖,无特殊意义:

@Component
public class ArbitraryDependency {

    private final String label = "Arbitrary Dependency";

    public String toString() {
        return label;
    }
}

以下是相关集成测试 FieldInjectTest

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestInjectType.class)
public class FieldInjectIntegrationTest {

    @Inject
    private ArbitraryDependency fieldInjectDependency;

    @Test
    public void givenInjectAnnotation_WhenOnField_ThenValidDependency(){
        assertNotNull(fieldInjectDependency);
        assertEquals("Arbitrary Dependency",
          fieldInjectDependency.toString());
    }
}

与优先按名称匹配的 @Resource 注解不同,@Inject 注解的默认行为是按类型匹配依赖。

这意味着即使类引用变量名称与 bean 名称不同,只要 bean 在应用上下文中定义,依赖仍会被解析。注意以下测试中的引用变量名称:

@Inject
private ArbitraryDependency fieldInjectDependency;

与应用上下文中配置的 bean 名称不同:

@Bean
public ArbitraryDependency injectDependency() {
    ArbitraryDependency injectDependency = new ArbitraryDependency();
    return injectDependency;
}

执行测试时,依赖被成功解析。

3.1.2. 按限定符匹配

如果存在特定类的多个实现,且某个类需要特定 bean 怎么办?修改集成测试示例使其需要另一个依赖。

本示例中,我们子类化 ArbitraryDependency 类(用于按类型匹配的示例)创建 AnotherArbitraryDependency 类:

public class AnotherArbitraryDependency extends ArbitraryDependency {

    private final String label = "Another Arbitrary Dependency";

    public String toString() {
        return label;
    }
}

每个测试用例的目标是确保将每个依赖正确注入到每个引用变量:

@Inject
private ArbitraryDependency defaultDependency;

@Inject
private ArbitraryDependency namedDependency;

使用 FieldQualifierInjectTest 集成测试演示按限定符匹配:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestInjectQualifier.class)
public class FieldQualifierInjectIntegrationTest {

    @Inject
    private ArbitraryDependency defaultDependency;

    @Inject
    private ArbitraryDependency namedDependency;

    @Test
    public void givenInjectQualifier_WhenOnField_ThenDefaultFileValid(){
        assertNotNull(defaultDependency);
        assertEquals("Arbitrary Dependency",
          defaultDependency.toString());
    }

    @Test
    public void givenInjectQualifier_WhenOnField_ThenNamedFileValid(){
        assertNotNull(defaultDependency);
        assertEquals("Another Arbitrary Dependency",
          namedDependency.toString());
    }
}

如果应用上下文中存在特定类的多个实现,且 FieldQualifierInjectTest 集成测试尝试按以下方式注入依赖,将抛出 NoUniqueBeanDefinitionException

@Inject 
private ArbitraryDependency defaultDependency;

@Inject 
private ArbitraryDependency namedDependency;

抛出此异常是 Spring 框架在指出存在特定类的多个实现,无法确定使用哪一个。为消除歧义,需修改 FieldQualifierInjectTest 集成测试的第 7 行和第 10 行:

@Inject
private ArbitraryDependency defaultDependency;

@Inject
private ArbitraryDependency namedDependency;

将所需 bean 名称传递给与 @Inject 注解一起使用的 @Qualifier 注解。修改后的代码块如下:

@Inject
@Qualifier("defaultFile")
private ArbitraryDependency defaultDependency;

@Inject
@Qualifier("namedFile")
private ArbitraryDependency namedDependency;

@Qualifier 注解要求 bean 名称严格匹配。必须确保正确传递 bean 名称给 Qualifier,否则将抛出 NoUniqueBeanDefinitionException。重新运行测试,测试将通过。

3.1.3. 按名称匹配

用于演示按名称匹配的 FieldByNameInjectTest 集成测试与按类型匹配的执行路径类似。唯一区别是现在需要特定 bean 而非特定类型。本示例中,我们再次子类化 ArbitraryDependency 类生成 YetAnotherArbitraryDependency 类:

public class YetAnotherArbitraryDependency extends ArbitraryDependency {

    private final String label = "Yet Another Arbitrary Dependency";

    public String toString() {
        return label;
    }
}

使用以下集成测试演示按名称匹配的执行路径:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestInjectName.class)
public class FieldByNameInjectIntegrationTest {

    @Inject
    @Named("yetAnotherFieldInjectDependency")
    private ArbitraryDependency yetAnotherFieldInjectDependency;

    @Test
    public void givenInjectQualifier_WhenSetOnField_ThenDependencyValid(){
        assertNotNull(yetAnotherFieldInjectDependency);
        assertEquals("Yet Another Arbitrary Dependency",
          yetAnotherFieldInjectDependency.toString());
    }
}

应用上下文配置如下:

@Configuration
public class ApplicationContextTestInjectName {

    @Bean
    public ArbitraryDependency yetAnotherFieldInjectDependency() {
        ArbitraryDependency yetAnotherFieldInjectDependency =
          new YetAnotherArbitraryDependency();
        return yetAnotherFieldInjectDependency;
    }
}

运行集成测试时,测试将通过。

为验证依赖确实通过按名称匹配的执行路径注入,需将传递给 @Named 注解的值 yetAnotherFieldInjectDependency 更改为其他名称。重新运行测试时,将抛出 NoSuchBeanDefinitionException

3.2. Setter 注入

@Inject 注解的基于 setter 的注入与 @Resource 的基于 setter 的注入方法类似。不是注解引用变量,而是注解对应的 setter 方法。基于字段的依赖注入所遵循的执行路径同样适用于基于 setter 的注入。

4. @Autowired 注解

@Autowired 注解的行为与 @Inject 注解类似。唯一区别是 @Autowired 注解属于 Spring 框架。该注解的执行路径与 @Inject 相同,按优先级顺序列出:

  1. 按类型匹配
  2. 按限定符匹配
  3. 按名称匹配

这些执行路径同时适用于 setter 注入和字段注入。

4.1. 字段注入

4.1.1. 按类型匹配

演示 @Autowired 按类型匹配执行路径的集成测试示例与演示 @Inject 按类型匹配的测试类似。使用以下 FieldAutowiredTest 集成测试演示使用 @Autowired 注解的按类型匹配:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestAutowiredType.class)
public class FieldAutowiredIntegrationTest {

    @Autowired
    private ArbitraryDependency fieldDependency;

    @Test
    public void givenAutowired_WhenSetOnField_ThenDependencyResolved() {
        assertNotNull(fieldDependency);
        assertEquals("Arbitrary Dependency", fieldDependency.toString());
    }
}

此集成测试的应用上下文配置如下:

@Configuration
public class ApplicationContextTestAutowiredType {

    @Bean
    public ArbitraryDependency autowiredFieldDependency() {
        ArbitraryDependency autowiredFieldDependency =
          new ArbitraryDependency();
        return autowiredFieldDependency;
    }
}

使用此集成测试证明按类型匹配优先于其他执行路径。注意 FieldAutowiredTest 集成测试第 8 行的引用变量名称:

@Autowired
private ArbitraryDependency fieldDependency;

与应用上下文中的 bean 名称不同:

@Bean
public ArbitraryDependency autowiredFieldDependency() {
    ArbitraryDependency autowiredFieldDependency =
      new ArbitraryDependency();
    return autowiredFieldDependency;
}

运行测试时,测试将通过。

为确认依赖确实通过按类型匹配的执行路径解析,需更改 fieldDependency 引用变量的类型并重新运行集成测试。这次 FieldAutowiredTest 集成测试将失败,并抛出 NoSuchBeanDefinitionException。这验证了我们使用按类型匹配解析依赖。

4.1.2. 按限定符匹配

如果面临在应用上下文中定义多个 bean 实现的情况:

@Configuration
public class ApplicationContextTestAutowiredQualifier {

    @Bean
    public ArbitraryDependency autowiredFieldDependency() {
        ArbitraryDependency autowiredFieldDependency =
          new ArbitraryDependency();
        return autowiredFieldDependency;
    }

    @Bean
    public ArbitraryDependency anotherAutowiredFieldDependency() {
        ArbitraryDependency anotherAutowiredFieldDependency =
          new AnotherArbitraryDependency();
        return anotherAutowiredFieldDependency;
    }
}

如果执行以下 FieldQualifierAutowiredTest 集成测试,将抛出 NoUniqueBeanDefinitionException

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestAutowiredQualifier.class)
public class FieldQualifierAutowiredIntegrationTest {

    @Autowired
    private ArbitraryDependency fieldDependency1;

    @Autowired
    private ArbitraryDependency fieldDependency2;

    @Test
    public void givenAutowiredQualifier_WhenOnField_ThenDep1Valid(){
        assertNotNull(fieldDependency1);
        assertEquals("Arbitrary Dependency", fieldDependency1.toString());
    }

    @Test
    public void givenAutowiredQualifier_WhenOnField_ThenDep2Valid(){
        assertNotNull(fieldDependency2);
        assertEquals("Another Arbitrary Dependency",
          fieldDependency2.toString());
    }
}

此异常由应用上下文中定义的两个 bean 引起的歧义导致。Spring 框架不知道应将哪个 bean 依赖自动装配到哪个引用变量。通过在 FieldQualifierAutowiredTest 集成测试的第 7 行和第 10 行添加 @Qualifier 注解可解决此问题:

@Autowired
private FieldDependency fieldDependency1;

@Autowired
private FieldDependency fieldDependency2;

修改后的代码块如下:

@Autowired
@Qualifier("autowiredFieldDependency")
private FieldDependency fieldDependency1;

@Autowired
@Qualifier("anotherAutowiredFieldDependency")
private FieldDependency fieldDependency2;

重新运行测试,测试将通过。

4.1.3. 按名称匹配

使用相同的集成测试场景演示使用 @Autowired 注解按名称匹配执行路径注入字段依赖。按名称自动装配依赖时,应用上下文 ApplicationContextTestAutowiredName 必须使用 @ComponentScan 注解:

@Configuration
@ComponentScan(basePackages={"com.baeldung.dependency"})
    public class ApplicationContextTestAutowiredName {
}

使用 @ComponentScan 注解扫描包中带有 @Component 注解的 Java 类。例如,在应用上下文中,com.baeldung.dependency 包将被扫描以查找带有 @Component 注解的类。此场景中,Spring 框架必须检测到带有 @Component 注解的 ArbitraryDependency 类:

@Component(value="autowiredFieldDependency")
public class ArbitraryDependency {

    private final String label = "Arbitrary Dependency";

    public String toString() {
        return label;
    }
}

传递给 @Component 注解的属性值 autowiredFieldDependency 告诉 Spring 框架:ArbitraryDependency 类是名为 autowiredFieldDependency 的组件。为使 @Autowired 注解按名称解析依赖,组件名称必须与 FieldAutowiredNameTest 集成测试中定义的字段名称对应(参见第 8 行):

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  loader=AnnotationConfigContextLoader.class,
  classes=ApplicationContextTestAutowiredName.class)
public class FieldAutowiredNameIntegrationTest {

    @Autowired
    private ArbitraryDependency autowiredFieldDependency;

    @Test
    public void givenAutowired_WhenSetOnField_ThenDependencyResolved(){
        assertNotNull(autowiredFieldDependency);
        assertEquals("Arbitrary Dependency",
          autowiredFieldDependency.toString());
    }
}

运行 FieldAutowiredNameTest 集成测试时,测试将通过。

但如何确定 @Autowired 注解确实调用了按名称匹配的执行路径?可将引用变量名称 autowiredFieldDependency 更改为其他名称并重新运行测试。

这次测试将失败并抛出 NoUniqueBeanDefinitionException。类似的验证是将 @Component 的属性值 autowiredFieldDependency 更改为其他值并重新运行测试。同样会抛出 NoUniqueBeanDefinitionException

此异常证明:如果使用错误的 bean 名称,将找不到有效 bean。这就是我们确定按名称匹配执行路径被调用的方式。

4.2. Setter 注入

@Autowired 注解的基于 setter 的注入与 @Resource 基于 setter 的注入方法类似。不是用 @Inject 注解引用变量,而是注解对应的 setter 方法。基于字段的依赖注入所遵循的执行路径同样适用于基于 setter 的注入。

5. 应用这些注解

这引出了一个问题:应在什么情况下使用哪个注解?答案取决于应用程序面临的设计场景,以及开发者希望如何利用基于每个注解默认执行路径的多态性。

5.1. 通过多态性全局使用单例

如果设计基于接口或抽象类的实现来定义应用行为,且这些行为在整个应用中使用,则可使用 @Inject@Autowired 注解。

此方法的优势在于:升级应用或应用补丁修复 bug 时,类可以被替换,且对整体应用行为的负面影响最小。此场景下,主要默认执行路径是按类型匹配。

5.2. 通过多态性实现细粒度应用行为配置

如果设计导致应用具有复杂行为,每种行为基于不同的接口/抽象类,且这些实现在应用中的使用各不相同,则可使用 @Resource 注解。此场景下,主要默认执行路径是按名称匹配。

5.3. 依赖注入应完全由 Jakarta EE 平台处理

如果设计要求所有依赖由 Jakarta EE 平台(而非 Spring)注入,则选择范围在 @Resource@Inject 注解之间。应根据所需的默认执行路径缩小最终选择。

5.4. 依赖注入应完全由 Spring 框架处理

如果要求所有依赖由 Spring 框架处理,唯一选择是 @Autowired 注解。

5.5. 讨论总结

下表总结了我们的讨论:

场景 @Resource @Inject @Autowired
通过多态性全局使用单例
通过多态性实现细粒度应用行为配置
依赖注入应完全由 Jakarta EE 平台处理
依赖注入应完全由 Spring 框架处理

6. 结论

本文旨在深入剖析每个注解的行为。理解每个注解的工作原理将有助于设计更优的应用程序并简化维护。

讨论中使用的代码可在 GitHub 上找到。