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 接口,其中有两个实现 - S3FileStorageAzureFileStorage。我们可以让 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