Spring 注入具有多个实现类的接口
1、简介
本文将带你了解如何在 Spring Boot 中自动装配具有多个实现的接口,以及一些常见用例。这是一个强大的功能,允许开发人员动态地将接口的不同实现注入到组件中。
2、默认行为
默认情况下,当接口存在多个实现并试图将该接口自动装配到组件中时,会遇到异常:“required a single bean, but X were found”。原因很简单: Spring 不知道我们想在该组件中注入哪个实现。幸运的是,Spring 提供了多种工具,可以明确要注入的实现。
3、@Qualifier
通过 @Qualifier
注解,我们可以在接口的多个实现组件中指定要自动装配的 Bean。我们可以将其应用到组件本身,为其自定义名称:
@Service
@Qualifier("goodServiceA-custom-name")
public class GoodServiceA implements GoodService {
// 实现
}
然后,用 @Qualifier
对参数进行注解,以指定想要的实现:
@Autowired
public SimpleQualifierController(
@Qualifier("goodServiceA-custom-name") GoodService niceServiceA,
@Qualifier("goodServiceB") GoodService niceServiceB,
GoodService goodServiceC
) {
this.goodServiceA = niceServiceA;
this.goodServiceB = niceServiceB;
this.goodServiceC = goodServiceC;
}
如上,使用自定义 Qualifier 自动装配了 GoodServiceA
。
而,GoodServiceB
实现却没有自定义 Qualifier:
@Service
public class GoodServiceB implements GoodService {
// 实现
}
这种情况下,通过类名对组件进行了自动装配。这种自动装配的限定符(Qualifier)使用驼峰形式,例如,如果类名是 MyAwesomeClass
,那么 myAwesomeClass
就是一个有效的限定符。
上述代码中的第三个参数(goodServiceC
)更加有趣。甚至不需要用 @Qualifier
来注解它,因为 Spring 默认会尝试按参数名自动装配组件,如果 GoodServiceC
存在,就不会注入失败:
@Service
public class GoodServiceC implements GoodService {
// 实现
}
4、@Primary
此外,还可以用 @Primary
来注解其中一个实现。如果有多个候选实现,且按参数名或 Qualifier 自动装配不适用,Spring 将使用该实现:
@Primary
@Service
public class GoodServiceC implements GoodService {
// 实现
}
当我们经常使用其中一种实现时,它就会非常有用,有助于避免出现 “required a single bean” 的异常。
5、@Profile
可以使用 Spring Profiles 机制来决定自装配哪个组件。
假如,我们有一个 FileStorage
接口,其中有两个实现 - S3FileStorage
和 AzureFileStorage
。我们可以让 S3FileStorage
只在 prod
Profile 中激活,而 AzureFileStorage
只在 dev
Profile 中激活。
@Service
@Profile("dev")
public class AzureFileStorage implements FileStorage {
// 实现
}
@Service
@Profile("prod")
public class S3FileStorage implements FileStorage {
// 实现
}
6、将实现自动装配到集合
Spring 允许我们将特定类型的所有可用 Bean 注入到一个集合中。
如下,将 GoodService
的所有实现自装配接到一个 List
中:
@Autowired
public SimpleCollectionController(List<GoodService> goodServices) {
this.goodServices = goodServices;
}
此外,还可以将所有实现自动装配到集合、Map 或数组中。使用 Map 时,格式通常是 Map<String, GoodService>
,其中 key 是 Bean 的名称,值是 Bean 实例本身:
@Autowired
public SimpleCollectionController(Map<String, GoodService> goodServiceMap) {
this.goodServiceMap = goodServiceMap;
}
public void printAllHellos() {
String messageA = goodServiceMap.get("goodServiceA").getHelloMessage();
String messageB = goodServiceMap.get("goodServiceB").getHelloMessage();
// 打印 message
}
注意:只要候选 Bean 处于活动状态,Spring 就会将所有候选 Bean 自动装配到集合中,而不会考虑 Qualifier 或参数名称。它会忽略注解了 @Profile
的、与 Profile 不匹配的 Bean。同样,只有在满足条件的情况下,Spring 才会包含注解为 @Conditional
的 Bean(更多详情在下一节中介绍)。
7、高级控制
Spring 允许我们对自动装配所选择的候选对象进行额外控制。
如果需要更精确的条件来确定哪个 Bean 可以成为自动装配的候选对象,可以用 @Conditional
对其进行注解。它应该有一个参数,其中包含一个实现 Condition
的类(它是一个函数式接口)。例如,以下是检查操作系统是否为 Windows 的条件:
public class OnWindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("os.name").toLowerCase().contains("windows");
}
}
下面是如何用 @Conditional
对组件进行注解的示例:
@Component
@Conditional(OnWindowsCondition.class)
public class WindowsFileService implements FileService {
@Override
public void readFile() {
// 实现
}
}
在上例中,只有当 OnWindowsCondition
中的 matches()
返回 true
时,WindowsFileService
才会成为自动装配的候选对象。
对于非集合类型的自动装配,在使用 @Conditional
注解时需要小心,因为如果同时有多个符合条件的 Bean,会导致错误。
此外,自动装配如果找不到候选对象,就会异常。因此,在将 @Conditional
与自动装配集成时,设置 可选注入 是有意义的。这样可以确保应用在没有找到合适的 Bean 时仍能继续运行,而不会出错。有两种方法可以实现这一点:
@Autowired(required = false)
private GoodService goodService; // 不安全,应该检查它是否为空
@Autowired
private Optional<GoodService> goodService; // 更加安全的方式
在自动装配到集合时,可以使用 @Order
注解指定组件的顺序:
@Order(2)
public class GoodServiceA implements GoodService {
// 实现
}
@Order(1)
public class GoodServiceB implements GoodService {
// 实现
}
如果尝试自动装配 List<GoodService>
,GoodServiceB
将被放在 GoodServiceA
之前。注意,当自动装配到 Set
时,@Order
不起作用。
8、总结
本文介绍了 Spring 在自动装配过程中用于管理接口的多个实现的工具。这些工具和技术使得在设计 Spring Boot 应用时可以采用更加动态的方式。然而,就像使用任何工具一样,应该确保使用它们的必要性,因为滥用可能会引入错误并增加后期维护的复杂性。
Ref:https://www.baeldung.com/spring-boot-autowire-multiple-implementations