Spring注入之@Resource、@Autowired以及@Inject的区别

1. 概述

这篇文章将演示与依赖注入相关的注解的使用,即@Resource@Inject@Autowired注解。这些注解为类提供了解析依赖关系的声明方式,也是 Spring 最重要的特性之一。例如:

1
2
@Autowired
ArbitraryClass arbObject;

而不是直接实例化(必要的方式),例如:

1
ArbitraryClass arbObject = new ArbitraryClass();

三个注解中的两个属于Java扩展包:javax.annotation.Resourcejavax.inject.Inject,它们隶属于 J2EE 规范,@Autowired 注解属于org.springframework.beans.factory.annotation 包。

这些注解中的每一个都可以通过 Field 注入 或通过 Setter 注入 来解决依赖。我们将使用一个简化但实用的示例来演示三个注解之间的区别,这些示例将重点介绍如何在测试用例期间使用三个注入注解测试所需的依赖关系。

2. @Resource 注解

@Resource注解是 J2EE 规范的一部分,来自JSR-250,该注解在选择候选 Bean 的时候,优先级顺序如下:

  1. byName
  2. byType
  3. @Qualifier

2.1 Field 注入

通过使用@Resource 注解对实例变量进行注解来实现通过字段注入来解决依赖关系。

2.1.1 byName 匹配

用于演示按名称匹配字段注入的测试用例出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceNameType.class)
public class FieldResourceInjectionTest {

@Resource(name="foobar")
private Foobar foobar;

@Test
public void givenResourceAnnotation_WhenOnField_ThenDependencyValid(){
assertNotNull(defaultFile);
}
}

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

1
2
@Resource(name="foobar")
private Foobar foobar;

此配置将使用按名称匹配的执行路径解决依赖关系。必须在ApplicationContext 中定义 名为 foobar 的bean 。

请注意,bean id 和相应的引用属性值必须匹配

1
2
3
4
5
6
7
8
@Configuration
public class ApplicationContextTestResourceNameType {

@Bean(name="foobar")
public Foobar foobar() {
return new Foobar();
}
}

如果在 ApplicationContext 中没有这个名字的 bean,将抛出org.springframework.beans.factory.NoSuchBeanDefinitionException,这个可以通过在用例中更改@Resource注解的 name 属性值来测试一下。

2.1.2 byType

要演示按类型匹配,只需删除FieldResourceInjectionTest测试第7行的 name 属性值,使其如下所示:

1
2
@Resource
private File defaultFile;

并再次运行测试,测试仍然会通过,因为如果 @Resource 注解没有收到bean名称作为属性值,则Spring 将继续使用下一级优先级(按类型匹配),以尝试解析依赖项。

2.1.3 @Qualifier

为了演示按 @Qualifier 匹配的选择过程,修改测试用例场景,以便在ApplicationContextTestResourceQualifier 中定义两个bean :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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;
}
}

测试用例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceQualifier.class)
public class QualifierResourceInjectionTest {

@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。抛出此异常是因为 ApplicationContext 找到了两个类型为File bean定义,并且对哪个bean应该解析依赖关系感到困惑。

要解决此问题,需要在依赖注入的字段上添加 @Qualifier 注解,来指定空间究竟想使用哪个依赖项:

1
2
3
4
5
6
7
@Resource
@Qualifier("defaultFile")
private File dependency1;

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

再次运行测试用例,这次它应该通过。此测试的目的是证明即使在 ApplicationContext 中定义了多个bean,也可以通过 @Qualifier 来告诉 Spring 该用哪一个。

2.2 Setter 注入

除了在字段上进行注解注入外,Spring 还支持在 Setter 上进行依赖注入。

2.2.1 byName

唯一的区别是MethodResourceInjectionTest测试用例有一个 Setter 方法,我们在 Setter 方法上做注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceNameType.class)
public class MethodResourceInjectionTest {

private File defaultFile;

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

@Test
public void givenResourceAnnotation_When *Setter* _ThenDependencyValid(){
assertNotNull(defaultFile);
assertEquals("namedFile.txt", defaultFile.getName());
}
}

通过 Setter 注入来解决依赖关系是通过注解引用变量的相应 Setter 方法来完成的。

2.2.2 byType

同样的,只是将 @Resource 注解从 Field 移到对应的 Setter 方法上(并去掉 name 属性):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceNameType.class)
public class MethodByTypeResourceTest {

private File defaultFile;

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

@Test
public void givenResourceAnnotation_When *Setter* _ThenValidDependency(){
assertNotNull(defaultFile);
assertEquals("namedFile.txt", defaultFile.getName());
}
}

2.2.3 @Qualifier

MethodByQualifierResourceTest测试用例将被用来演示 Qualifier 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceQualifier.class)
public class MethodByQualifierResourceTest {

private File arbDependency;
private File anotherArbDependency;

@Test
public void givenResourceQualifier_When *Setter* _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;
}
}

3. @Inject 注解

@Inject注解属于JSR-330规范,它挑选候选者的优先级如下:

  1. byType
  2. @Qualifier
  3. byName

要启用 @Inject 注解,需要添加相应的Maven依赖:

1
2
3
4
5
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>

这个注解使用率相对较低,这里不再详细的介绍每个场景下的单元测试用例,基本上和前面的使用方式一样,除了将 @Resource 修改为 @Inject 即可。

3.1.3 byName

这里单独把 byName 的场景拿出来是因为跟 @Resource 相比,它有一些不一样的地方,它并不是在 @Inject 中添加 name 属性来完成,而是需要另外一个注解(@Named)配合,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceNameType.class)
public class MethodResourceInjectionTest {

private File defaultFile;

// 注意这里采用的是 *@Named* 注解而非 *@Inject* 的属性
@Inject
@Named("namedFile")
protected void setDefaultFile(File defaultFile) {
this.defaultFile = defaultFile;
}

@Test
public void givenResourceAnnotation_When *Setter* _ThenDependencyValid(){
assertNotNull(defaultFile);
assertEquals("namedFile.txt", defaultFile.getName());
}
}

4. @Autowired 注解

@Autowired注解的行为类似于@Inject注解。唯一的区别是@Autowired注解是Spring框架的一部分。此注解具有与@Inject注解相同候选者选择优先顺序:

  1. byType
  2. @Qualifier
  3. byName

同样都适用于 SetterField 注入。
@Autowired 对于候选者的选择优先级是和 @Inject 一致的,但是其使用方式又和 @Resoure 是一样的,大家都应该很熟悉了,这里也不再详细的列举测试用例。

5. 如何选择?

既然我们知道了有三种不同的注解都可以完成依赖注入,那么,在哪些场景应该使用哪种注解呢?这些问题的答案取决于相关应用程序所面临的设计方案,以及开发人员希望如何根据每个注解的默认执行路径。

5.1 基于接口或抽象类的场景

如果设计的组件是基于接口或抽象类的实现,并且这些行为在整个应用程序中使用,则优先使用@Inject@Autowired 注解。因为这两个注解对于候选者的选择优先级中:默认是 byType 的。

5.2 拥有多个接口或抽象类实现的场景

如果设计是应用程序具有复杂行为,则每个行为都基于不同的接口/抽象类,且实现类都不止一种,那么应该优先使用@Resource注解。在此方案中,注解对于候选者的选择优先级中:默认是 byName 的。

5.3 基于 Java EE 平台的场景

如果Java EE平台而不是Spring注入所有依赖项的时候,那么选择在@Resource注解和@Inject注解之间,同时应该根据前述的两种不同的场景来决策到底使用哪一种。

5.4 基于 Spring 的场景

这里是说完全基于 Spring 场景,而没有 J2EE 的耦合,则唯一的选择是 @Autowired 注解。

6. 总结

根据前面的讲解,我们总结一下这三个注解对于候选者选择优先级顺序,以及其适用的场景。

6.1 候选者选择优先级

注解 第一顺位 第二顺位 第三顺位
@Resource byName byType @Qualifier
@Inject byType @Qualifier byName
@Autowired byType @Qualifier byName

6.2 注解选择

根据前面的讨论得出的注解选择方式如下表所示:

场景 @Resource @Inject @Autowired
基于接口或抽象类
接口或抽象类多实现
基于 J2EE 平台
基于 Spring 平台