SpringBoot中根据属性动态注册Spring Bean

在本教程中,我们将探讨如何根据自定义属性动态注册Bean 。我们将探讨BeanDefinitionRegistryPostProcessor 接口以及如何使用它向应用程序上下文添加 bean。

让我们首先创建一个简单的 Spring Boot 应用程序。

首先,我们将定义一个要动态注册的 bean。然后,我们将提供一个属性来决定如何注册 beans。最后,我们将定义一个配置类,它将根据我们的自定义属性注册 bean。

添加 Maven 依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>3.2.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>3.2.3</version>
    <scope>test</scope>
</dependency>

我们需要添加spring-boot-starter 和 spring-boot-starter-test 依赖项。

接下来,我们定义一个要根据自定义应用程序属性注册的 API 客户端:

public class ApiClient {
    private String name;
    private String url;
    private String key;
    // standard getters, setters and constructors
    
    public String getConnectionProperties() {
        return "Connecting to " + name + " at " + url;     
    }
}

假设我们想要使用这个 bean 根据我们提供的属性连接到不同的 API。我们不想为每个 API 创建类定义。相反,我们希望为每个 API 动态定义属性并注册 bean。

我们不应该使用@Component 或 @Service注释ApiClient 类  ,因为我们不想使用组件扫描将其注册为 bean。

让我们添加一个属性来确定 bean 应注册哪些 API。我们将在application.yml 文件中定义此属性:

api:
  clients:
    - name: example  
      url: https://api.example.com
      key: 12345
    - name: anotherexample
      url: https://api.anotherexample.com
      key: 67890

在这里,我们定义了两个客户端及其各自的属性。我们将在注册 bean 时使用这些属性。

动态注册Bean
Spring 提供了一种使用BeanDefinitionRegistryPostProcessor接口动态注册 bean 的方法  。 该接口允许我们在注册带注释的 bean 定义后添加或修改 bean 定义。 由于它发生在 bean 实例化之前,因此 bean 在应用程序上下文完全初始化之前注册。

BeanDefinitionRegistry后处理器
定义一个配置类,它将根据自定义属性注册 ApiClient beans :

public class ApiClientConfiguration implements BeanDefinitionRegistryPostProcessor {
    private static final String API_CLIENT_BEAN_NAME = "apiClient_";
    List<ApiClient> clients;
    public ApiClientConfiguration(Environment environment) {
        Binder binder = Binder.get(environment);
        List<HashMap> properties = binder.bind("api.clients", Bindable.listOf(HashMap.class)).get();
        clients = properties.stream().map(client -> new ApiClient(String.valueOf(client.get("name")),
                String.valueOf(client.get("url")), String.valueOf(client.get("key")))).toList();
    }    
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        clients.forEach(client -> {
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ApiClient.class);
            builder.addPropertyValue("name", client.getName());
            builder.addPropertyValue("url", client.getUrl());
            builder.addPropertyValue("key", client.getkey());
            registry.registerBeanDefinition(API_CLIENT_BEAN_NAME + client.getName(), builder.getBeanDefinition());
        });
    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }
}

在这里,我们实现了BeanDefinitionRegistryPostProcessor接口。我们重写postProcessBeanDefinitionRegistry方法,该方法负责根据我们的自定义属性注册 bean。

首先,我们定义一个常量API_CLIENT_BEAN_NAME,它将用作 bean 名称的前缀。在构造函数中,我们 使用 Binder API 从环境对象中读取属性。然后,我们使用这些属性创建ApiClient 对象。

在实现postProcessBeanDefinitionRegistry()方法时,我们迭代属性并 使用 BeanDefinitionRegistry对象注册ApiClient beans  。

我们使用 BeanDefinitionBuilder 创建 bean 。它要求我们定义bean类。然后它让我们使用字段名称一一设置 bean 属性。

请注意,我们使用唯一的名称注册每个 bean – API_CLIENT_BEAN_NAME + client.getName()。当我们想要从上下文中读取我们选择的 bean 时,这将很有帮助。

最后,我们需要定义主应用程序类并使用 @SpringBootApplication注解:

@SpringBootApplication
public class RegistryPostProcessorApplication {
    public static void main(String[] args) {
        SpringApplication.run(RegistryPostProcessorApplication.class, args);
    }
    @Bean
    public ApiClientConfiguration apiClientConfiguration(ConfigurableEnvironment environment) {
        return new ApiClientConfiguration(environment);
    }
}

在这里,我们定义ApiClientConfiguration bean 并将ConfigurableEnvironment 对象传递给构造函数。这将帮助我们读取ApiClientConfiguration类中的属性  。

现在 Bean 已注册,让我们测试它们是否具有连接到 API 的正确属性。为了测试这一点,我们将编写一个简单的测试类:

@SpringBootTest
class ApiClientConfigurationTest {
    @Autowired
    private ApplicationContext context;
    
    @Test
    void givenBeansRegistered_whenConnect_thenConnected() {
        ApiClient exampleClient = (ApiClient) context.getBean("apiClient_example");
        Assertions.assertEquals("Connecting to example at https://api.example.com", exampleClient.getConnectionProperties());
        
        ApiClient anotherExampleClient = (ApiClient) context.getBean("apiClient_anotherexample");
        Assertions.assertEquals("Connecting to anotherexample at https://api.anotherexample.com", anotherExampleClient.getConnectionProperties());
    }
}

在这里,我们使用 @SpringBootTest注释来加载应用程序上下文。然后,我们使用 ApplicationContext对象通过getBean()方法 从上下文中获取 bean   。 getBean  () 方法将唯一的 bean 名称作为参数,并从上下文中返回该 bean。

该测试检查 Bean 是否已正确注册并设置了正确的连接属性。