1. 概述
在 Spring 框架教程中,我们将演示如何使用与依赖注入相关的注解,即 @Resource
、@Inject
和 @Autowired
。这些注解为类提供了声明式依赖解析方式:
@Autowired
ArbitraryClass arbObject;
与直接实例化(命令式方式)形成对比:
ArbitraryClass arbObject = new ArbitraryClass();
三个注解中有两个属于 Java 扩展包:javax.annotation.Resource
和 javax.inject.Inject
。@Autowired
注解则属于 org.springframework.beans.factory.annotation
包。
每个注解都支持字段注入和 setter 注入两种依赖解析方式。我们将通过一个简化但实用的示例,基于每个注解的执行路径,演示三者的区别。
示例将重点展示在集成测试中如何使用这三种注入注解。测试所需的依赖可以是任意文件或任意类。
2. @Resource
注解
@Resource
注解属于 JSR-250 注解集合,随 Jakarta EE 一起提供。该注解按以下优先级顺序执行:
- 按名称匹配
- 按类型匹配
- 按限定符匹配
这些执行路径同时适用于 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 注解集合。该注解按以下优先级顺序执行:
- 按类型匹配
- 按限定符匹配
- 按名称匹配
这些执行路径同时适用于 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
相同,按优先级顺序列出:
- 按类型匹配
- 按限定符匹配
- 按名称匹配
这些执行路径同时适用于 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 上找到。