一个 Spring 依赖注入的问题浅析

使用 Spring 做项目的先思考一个问题,一个接口有在多个实现类的情况下,在成员变量的声明上如果没有指定注入那个别名的 Bean 的时候 Spring 会如何选择对应的 Bean 来进行注入?

问题模拟

先来简单说明一下示例代码。

定义一个接口,用于打印当前注入的 Bean 的名称

1
2
3
public interface TestService {
void printBeanName();
}

针对上面定义的接口编写两个实现类,并分别定义其 beanName 为 testService 和 testService2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service("testService")
public class TestServiceImpl implements TestService {

@Override
public void printBeanName() {
System.out.println("testService");
}
}

@Service("testService2")
public class TestServiceImpl2 implements TestService {

@Override
public void printBeanName() {
System.out.println("testService2");
}
}

编写 Handler,并注入 TestService

1
2
3
4
5
6
7
8
9
10
@Service
public class TestHandler {

@Autowired
private TestService testService;

public void test() {
testService.printBeanName();
}
}

编写单元测试,并观察输出

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
class TestHandlerTest {

@Autowired
private TestHandler testHandler;

@Test
void test() {
testHandler.test();
}
}

执行该单元测试,发现其打印的 beanName 为 testService。从输出结果可以看出在这种情况下最终注入的是 beanName 为 testService 的 bean,与组合交易子系统的现象一致。


源码分析与验证

根据 Spring 的文档,使用 Autowired 注解在自动装配时会通过 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties 方法来对属性进行赋值操作。我们可以在该方法入口打上断点,并对断点指定条件,如下图:

由于方法深度较深,调试的过程这里不再赘述,我们直接分析影响我们最终结果的方法 org.springframework.beans.factory.support.DefaultListableBeanFactory#determineAutowireCandidate,其源码及分析如下:

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
30
31
/**
* @param candidates 为我们要注入的属性对应的所有 Bean 组成的 map
* @param descriptor 描述了我们要注入的属性的一些信息,包含了属性对应的类、方法、fieldName 等信息
*/
protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {
// requiredType 为我们要注入的属性的类型,针对本的例子 requiredType 的值为 TestService 这个类
Class<?> requiredType = descriptor.getDependencyType();
// 如果 candidates 中有 Bean 添加了 @Primary 注解,则会返回该 Bean 对应的 beanName
String primaryCandidate = determinePrimaryCandidate(candidates, requiredType);
if (primaryCandidate != null) {
return primaryCandidate;
}
// 如果 candidates 中的 Bean 没有 @Primary 注解,但是添加了 @Priority 注解,会解析 Bean 的优先级,返回优先级最高的 Bean 对应的 beanName
String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType);
if (priorityCandidate != null) {
return priorityCandidate;
}
// 降级逻辑
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
String candidateName = entry.getKey();
Object beanInstance = entry.getValue();
// 此时的 this.resolvableDependencies 是不包含本例子中的 TestService 对应的任一 bean 的
// descriptor.getDependencyName() 方法的处理中会返回要注入的属性所对应的 fieldName,本例子中的 fieldName 为 testService
// 最终如果要注入属性名跟 candidates 的 Bean 的 beanName 相同,则会返回对应的 beanName
if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||
matchesBeanName(candidateName, descriptor.getDependencyName())) {
return candidateName;
}
}
return null;
}

下面我们来验证一下源码中的我们未验证到的情况

使用 Primary 注解来标识主要的类

将 beanName 为 testService2 的 Bean 加上 Primary 注解并运行单元测试,其打印的 beanName 为 testService2。

使用 Priority 来标识 Bean 的优先级

去掉上面操作所添加的 Primary 注解,来验证使用 Priority 时的情况。

  • 将 beanName 为 testService2 的 bean 加上 @Priority(2) 注解并运行单元测试,其打印的 beanName 为 testService2;
  • 将 beanName 为 testService 的 bean 加上 @Priority(1) 注解,将 beanName 为 testService2 的 bean 加上 @Priority(2) 注解并运行单元测试,其打印的 beanName 为 testService;
  • 将 beanName 为 testService 的 bean 加上 @Priority(3) 注解,将 beanName 为 testService2 的 bean 加上 @Priority(2) 注解并运行单元测试,其打印的 beanName 为 testService2。

修改 TestHandler 中的属性名

去掉上面操作所添加的 Primary 和 Priority 注解,来验证根据属性名注入时的情况。

  • 将 TestHandler 中的属性名修改为 testService2 并运行单元测试,其打印的 beanName 为 testService2;
  • 将 TestHandler 中的属性名修改为 testService3 并运行单元测试,程序抛出 NoUniqueBeanDefinitionException 异常。

最佳实践建议

在实际开发中,当遇到同一接口有多个实现类的情况时,推荐使用以下方式明确指定要注入的Bean:

使用 @Qualifier 注解

1
2
3
4
5
6
7
8
9
10
11
@Service
public class TestHandler {

@Autowired
@Qualifier("testService2")
private TestService testService;

public void test() {
testService.printBeanName();
}
}

使用字段名匹配

1
2
3
4
5
6
7
8
9
10
@Service
public class TestHandler {

@Autowired
private TestService testService2; // 字段名与Bean名匹配

public void test() {
testService2.printBeanName();
}
}

使用 @Primary 注解

1
2
3
4
5
@Service("testService")
@Primary
public class TestServiceImpl implements TestService {
// ...
}

结论

根据上面源码的分析与实践,同一类型存在多个 Bean 的情况下,在使用 Autowired 注解时 Spring 会采用以下装配顺序来选择要装配的 Bean:

  1. 使用了 Primary 注解的 Bean;
  2. 查找使用了 Priority 注解的 Bean,并选择优先级最高的;
  3. 要注入的属性的属性名称与 Bean 名称相同的;

在我们平时写代码时,为了代码的可读性和明确性,建议使用 Qualifier 注解来明确指明要注入的Bean,避免依赖默认的装配规则。