根据属性(Properties)动态注册 Spring Bean

1、概览

本文将带你了解如何根据自定义属性动态注册 Bean。

主要是学习 BeanDefinitionRegistryPostProcessor 接口,以及如何使用它将 Bean 添加到 Application Context 中。

2、设置

创建一个简单的 Spring Boot 应用。

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

2.1、依赖

添加 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-starterspring-boot-starter-test 依赖。

2.2、Bean 类

接下来,根据自定义 application properties 定义要注册的 ApiClient

public class ApiClient {
    private String name;
    private String url;
    private String key;
    // Getter、Setter 和构造函数
    
    public String getConnectionProperties() {
        return "Connecting to " + name + " at " + url;     
    }
}

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

这里不应该用 @Component@Service 来注解 ApiClient 类,避免组件扫描直接将其注册为 bean。

2.3、Properties

application.yml 文件中添加一个属性来确定应该为哪些 API 注册该 Bean

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

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

3、动态注册 Bean

Spring 提供了一种通过 BeanDefinitionRegistryPostProcessor 接口动态注册 Bean 的方式。这个接口允许我们在注解的 Bean 定义注册后添加或修改 Bean 定义。由于它在 Bean 实例化之前发生,因此这些 Bean 在 application context 完全初始化之前就被注册了。

3.1、BeanDefinitionRegistryPostProcessor

定义一个配置类,它将 根据自定义属性注册 ApiClient Bean:

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 名称的前缀。在构造函数中,使用 BinderAPIEnvironment 对象中读取属性。然后,使用这些属性创建 ApiClient 对象。

在执行 postProcessBeanDefinitionRegistry() 方法时,我们会遍历属性,并使用 BeanDefinitionRegistry 对象注册 ApiClient Bean。

使用 BeanDefinitionBuilder 创建 Bean,指定 Bean 类。然后,可以使用字段名逐一设置 Bean 属性。

注意,这里为每个 Bean 注册了一个唯一的名称: API_CLIENT_BEAN_NAME + client.getName()。这有助于我们从 Context 中读取我们选择的 Bean。

3.2、Application 类

最后,定义 main 类,并用 @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 类中的属性。

4、测试

现在,Bean 已经注册完成,编写一个简单的测试类进行验证:

@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 注解来加载 application context。然后,使用 ApplicationContext 对象,通过 getBean() 方法从 context 中获取 BeangetBean() 方法将唯一的 Bean 名称作为参数,并从 context 中返回 Bean。

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

5、总结

本文介绍了如何通过 BeanDefinitionRegistryPostProcessor 接口根据自定义属性动态注册 Spring Bean,以及如何从上下文中检索并使用 Bean。


Ref:https://www.baeldung.com/spring-beans-dynamic-registration-properties