环境:SpringBoot3.4.2
1. 简介
在开发中,常常会遇到有多个相同类型 Bean 的情况,这时 Spring 究竟如何精准找到合适的 Bean 进行注入呢?
当面临多个相同类型 Bean 时,Spring 不会随意选择,而是遵循一套严谨的策略,层层筛选。从Spring Boot3(Spring 6)开始将通过 7种 策略进行筛选Bean。
对于开发者而言,了解这些策略至关重要。它能帮助我们在复杂的应用架构中,更好地掌控 Bean 的注入行为,避免因 Bean 冲突导致的问题。同时,也能让我们在设计应用时,更加合理地规划 Bean 的定义和依赖关系,充分发挥 Spring 框架的强大功能,提升开发效率和代码质量。
接下来,我将详细的介绍这 7种 注入策略。
2.实战案例
准备环境
// 准备接口
public interface GreetingService {
void greet(String name);
}
// 下面是该接口的3个实现
@Service
public class ChineseGreetingService implements GreetingService {
@Override
public void greet(String name) {
System.out.println("你好, " + name + "!");
}
}
@Service
public class EnglishGreetingService implements GreetingService {
@Override
public void greet(String name) {
System.out.println("Hello, " + name + "!");
}
}
@Service
public class FrenchGreetingService implements GreetingService {
@Override
public void greet(String name) {
System.out.println("Bonjour, " + name + " !");
}
}
下面这个类用来注入 GreetingService 接口
@Service
public class CommonService {
private final GreetingService greetingService ;
public CommonService(GreetingService greetingService) {
this.greetingService = greetingService;
}
public void say() {
this.greetingService.greet("Spring全家桶实战案例") ;
}
}
当前Spring容器中有3个GreetingService接口类型的Bean,启动Spring容器后将看到如下的错误:
要求一个GreetingService bean,实际发现了3个。
接下来,我们将详细的介绍对于此种情况Spring是采用的哪 6 种策略处理此种情况。
2.1 查找@Primary
当容器中存在多个相同类型的bean时,Spring会优先选择带有 @Primary 注解的bean作为首选实现。如下代码:
@Service
@Primary
public class ChineseGreetingService implements GreetingService{}
我们将其中一个Bean使用@Primary,容器启动就正确了。而该ChineseGreetingService将被注入到CommonService。
若在Spring配置中存在多个带有@Primary注解的同类型bean,容器在自动装配时将因无法明确优先级而抛出
NoUniqueBeanDefinitionException,导致应用启动失败。如下示例:
@Service
@Primary
public class EnglishGreetingService implements GreetingService {}
@Service
@Primary
public class ChineseGreetingService implements GreetingService{}
再次启动容器,抛出如下错误:
2.2 查找@Fallback
当没有任何一个 Bean 被标注为 @Primary 时,Spring 容器会进一步检查是否存在唯一一个未被 @Fallback 注解标记的 Bean,并将其作为候选进行自动注入。
修改代码如下:
@Service
public class ChineseGreetingService implements GreetingService
@Service
@Fallback
public class EnglishGreetingService implements GreetingService
@Service
@Fallback
public class FrenchGreetingService implements GreetingService
我们将其中2个bean使用 @Fallback 注解,标记为候选bean,这样做以后,我们的容器就能成功的注入 ChineseGreetingService。
注意:你只能留一个没有使用@Fallback的bean,否则启动还是报错。
2.3 通过名称匹配
当通过@Primary与@Fallback都不能正确的处理,那么接下来就要开始通过beanName进行匹配。修改代码如下(记得不要忘记将使用@Primary和@Fallback注解的地方都删除):
@Service
public class CommonService {
private final GreetingService chineseGreetingService ;
public CommonService(GreetingService chineseGreetingService) {
this.chineseGreetingService = chineseGreetingService;
}
// ...
}
在代码中需要注入依赖的位置,将注入变量的名称修改为与目标 Bean 的名称(beanName)完全一致。
2.4 使用@Qualifier
若在上述检查后仍未找到合适的 Bean,Spring 容器会进一步检查目标注入字段是否标注了 @Qualifier 注解。修改代码如下:
@Service
public class CommonService {
private final GreetingService greetingService ;
public CommonService(@Qualifier("chineseGreetingService") GreetingService greetingService) {
this.greetingService = greetingService;
}
}
构造函数的参数上明确指定了将要注入哪个Bean。
思考,如果我们将代码修改如下,会注入谁?修改代码如下:
private final GreetingService englishGreetingService ;
public CommonService(@Qualifier("chineseGreetingService") GreetingService englishGreetingService) {
this.englishGreetingService = englishGreetingService;
System.err.println(this.englishGreetingService.getClass()) ;
}
最终控制输出:
class com.pack.ioc.multi_bean_strategy.ChineseGreetingService
注入的是由 @Qualifier 设置的bean。
下面我们再将代码修改如下:
@Service
public class CommonService {
@Resource
@Qualifier("chineseGreetingService")
private GreetingService englishGreetingService ;
@PostConstruct
public void init() {
System.err.println(this.englishGreetingService.getClass()) ;
}
}
控制台输出:
class com.pack.ioc.multi_bean_strategy.EnglishGreetingService
字段注入与构造函数的注入是有一定区别的,所以我们这里需要注意。
2.5 使用@Priority
到这里还是没有合适的bean,那么就开始检查这些bean是否使用了@Priority注解(定义bean的优先级)。修改代码如下:
@Service
@Priority(1)
public class ChineseGreetingService
@Service
@Priority(2)
public class EnglishGreetingService
@Service
@Priority(3)
public class FrenchGreetingService
说明:@Priority 注解指定的值越小优先级就越高。所以,通过上面的修改后,将成功的注入ChineseGreetingService。
2.6 设置默认候选Bean
从Spring 6.2 起,可以通过 @Bean 注解配置 defaultCandidate 属性,该属性用来指定:在仅基于纯类型(plain type)且不考虑任何额外限定条件(例如是否匹配限定符 qualifier)的情况下,此 Bean 是否能成为其他 Bean 自动注入(autowired)的候选对象。
我们这里需要通过@Bean注册bean,修改代码如下:
@Bean(defaultCandidate = true)
GreetingService chineseGreetingService() {
return new ChineseGreetingService() ;
}
@Bean(defaultCandidate = false)
GreetingService englishGreetingService() {
return new EnglishGreetingService() ;
}
@Bean(defaultCandidate = false)
GreetingService frenchGreetingService() {
return new FrenchGreetingService() ;
}
该属性值默认为true,将其它2个都置为false,这样我们也可以成功的注入bean(ChineseGreetingService)。
2.7 直接使用已注册的Bean
这最后一种比较特殊,我们可以将要被注入的对象提前的注册到BeanFactory中。直接上代码吧。我们首先将3个实现类恢复到初始状态,代码如下:
@Service
public class ChineseGreetingService
@Service
public class EnglishGreetingService
@Service
public class FrenchGreetingService
// CommonService修改
@Resource
private GreetingService greetingService ;
@PostConstruct
public void init() {
System.err.println(this.greetingService.getClass()) ;
}
接下来,我们自定义 BeanFactoryPostProcessor
@Component
public class PackBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 这里我们直接指定了只要是GreetingService类型的注入,那就直接注入EnglishGreetingService
beanFactory.registerResolvableDependency(
GreetingService.class, new EnglishGreetingService()) ;
}
}
通过此种方式,不管你容器有多少个同类型的bean,都将只会注入这里指定的对象。
我们可以直接在代码中注入 HttpServletRequest,也是通过此种方式实现的。
以上就是当同类型的Bean有多个时Spring底层处理的7种实现策略。