1. 概述

在这个教程中,我们将探讨如何根据自定义属性动态注册bean。我们将研究BeanDefinitionRegistryPostProcessor接口,以及如何利用它将bean添加到应用上下文。

2. 代码设置

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

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. 类类

接下来,我们定义一个根据自定义应用属性想要动态注册的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创建类定义。相反,我们希望定义属性并根据需要动态注册bean。

我们不应该在ApiClient类上使用@Component@Service注解,因为我们不希望通过组件扫描来注册它。

2.3. 属性

让我们添加一个属性,决定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时会使用这些属性。

3. 动态注册bean

Spring提供了通过BeanDefinitionRegistryPostProcessor接口动态注册bean的方法。这个接口允许我们在注解的bean定义注册后添加或修改bean定义。由于它发生在bean实例化之前,因此在应用上下文完全初始化之前就注册了bean。

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名称的前缀。在构造函数中,我们使用Binder API从Environment对象读取属性。然后,我们使用属性创建ApiClient对象。

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

我们使用BeanDefinitionBuilder创建bean,需要定义bean类。然后,我们可以逐一设置bean属性,使用字段名。

注意,我们为每个bean注册一个唯一的名称——API_CLIENT_BEAN_NAME + client.getName()。这将帮助我们在需要时从上下文中读取所需的bean。

3.2. 主应用类

最后,我们需要定义主应用类,并用@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已经注册,让我们测试它们是否具有正确的属性来连接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是否正确注册并且设置了正确的连接属性。

5. 总结

在这个教程中,我们探讨了如何使用BeanDefinitionRegistryPostProcessor接口根据自定义属性动态注册Spring bean。我们还编写了一个简单的测试案例,展示了如何从上下文中检索bean并使用它们。

如往常一样,示例代码可在GitHub上找到此处