Dynamic Bean Registration Based on Properties
Spring offers a powerful approach to configuring applications: defining beans through annotations and properties. But what if we need to create beans dynamically based on configuration loaded at runtime? In this article, we’ll explore how to achieve dynamic bean registration using the BeanDefinitionRegistryPostProcessor
interface in a Spring Boot application.
1. Understanding Dynamic Bean Registration
Traditionally, beans in a Spring application are defined statically within XML configuration files or through Java-based configuration classes using annotations like @Component
, @Service
, or @Repository
. However, there are scenarios where static bean registration might not suffice. For instance, when dealing with a large number of similar beans or when the configuration of beans needs to be determined dynamically at runtime based on certain conditions.
Dynamic bean registration solves these challenges by allowing beans to be created and registered programmatically, based on custom properties or conditions defined within the application.
1.1 BeanDefinitionRegistryPostProcessor
The BeanDefinitionRegistryPostProcessor
interface in Spring Boot provides a way to modify bean definitions before they are registered with the application context. It allows for dynamic manipulation of bean definitions, making it an ideal candidate for dynamically registering beans based on custom properties.
2. Code Example: Dynamic Bean Registration with API Client
This section will demonstrate the dynamic bean registration process with a simplified example using Spring Boot. We will integrate an API client bean that communicates with an external API and register it dynamically based on custom properties.
2.1 Api Client Class
public class ApiClient { private String baseUrl; private String apiKey; public ApiClient(String baseUrl, String apiKey) { this.baseUrl = baseUrl; this.apiKey = apiKey; } public ApiClient() { } public void showDetails() { System.out.println("ApiClient with key: " + apiKey + " located at " + baseUrl); } public String getBaseUrl() { return baseUrl; } public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } }
2.2 Custom Properties
Create a properties file application.yml
with entries defining the APIs
## YAML Template. --- api: clients: - apiKey: 1111 baseUrl: https://api.javacodegeeks.com/v1 - apiKey: 2222 baseUrl: https://examples.javacodegeeks.com/v1
The above YAML snippet describes an API configuration with multiple clients, each with its base URL (baseUrl
) and API key (apiKey
). The first client configuration defines a client with a base URL of http://api.javacodegeeks.com/v1
and an API key of 1111
and the second client configuration represents another client with a base URL of http://examples.javacodegeeks.com/v1
and an API key of 2222
.
2.3 Dynamic Registration Class
Next, we will create a configuration class responsible for registering the ApiClient
class. Below is the class that dynamically registers ApiClient
beans based on the properties defined in the environment loaded from the application.yml
file. Each ApiClient
bean is registered with a bean name derived from its API key.
public class AppConfig implements BeanDefinitionRegistryPostProcessor { List<ApiClient> clients; public AppConfig(Environment environment) { Binder binder = Binder.get(environment); clients = binder.bind("api.clients", Bindable.listOf(ApiClient.class)).get(); } @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { clients.forEach(client -> { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ApiClient.class); builder.addPropertyValue("apiKey", client.getApiKey()); builder.addPropertyValue("baseUrl", client.getBaseUrl()); registry.registerBeanDefinition("apiclient_" + client.getApiKey(), builder.getBeanDefinition()); }); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // Empty implementation, not required for this example } }
This block of code defines a Spring @Configuration
class named AppConfig
that implements the BeanDefinitionRegistryPostProcessor
interface. In the class, the constructor takes an Environment
object as a parameter. Inside the constructor, a Binder
is created using Binder.get(environment)
. Using the Binder
, the api.clients
property is bound to a list of ApiClient
objects. This list is stored in the clients
field.
The postProcessBeanDefinitionRegistry()
method is called by the Spring container during initialization after the bean definitions are registered but before the beans are instantiated. It iterates over the clients
list obtained from the environment and for each ApiClient
in the list, a BeanDefinition
is created using BeanDefinitionBuilder.genericBeanDefinition(ApiClient.class)
.
The properties of the ApiClient
object (apiKey
and baseUrl
) are set on the bean definition using builder.addPropertyValue()
and finally, the bean definition is registered with the bean registry using registry.registerBeanDefinition()
.
2.4 Configuration
Include AppConfig
in the main Spring configuration class.
@SpringBootApplication public class DynamicbeansregistrationApplication { public static void main(String[] args) { // SpringApplication.run(DynamicbeansregistrationApplication.class, args); ApplicationContext context = SpringApplication.run(DynamicbeansregistrationApplication.class, args); ApiClient bean = (ApiClient) context.getBean("apiclient_2222"); bean.showDetails(); } @Bean public AppConfig appConfiguration(ConfigurableEnvironment environment) { return new AppConfig(environment); } }
Above, we define a bean method named appConfiguration
that returns an instance of AppConfig
. This tells Spring to include this bean in the application context. The ConfigurableEnvironment
object is injected into the method as a parameter, which allows the environment properties to be passed to the AppConfig
constructor.
In the main method, The ApiClient
bean with the name apiclient_2222
is retrieved from the context using context.getBean()
and the showDetails()
method of the ApiClient
bean is invoked to display its details. The output is:
3. Testing Dynamic Bean Registration
We write integration tests to test the dynamic bean registration process to ensure that the API client bean is registered correctly based on the provided custom properties. Below is a simple test class using JUnit and Spring Boot’s testing utilities.
@SpringBootTest class DynamicbeansregistrationApplicationTests { @Autowired private ApplicationContext context; @Test void testApiClientBeanRegistration() { ApiClient apiClient = (ApiClient) context.getBean("apiclient_2222"); assertNotNull(apiClient); assertEquals("https://examples.javacodegeeks.com/v1", apiClient.getBaseUrl()); ApiClient anotherClient = (ApiClient) context.getBean("apiclient_1111"); assertNotNull(apiClient); assertEquals("https://api.javacodegeeks.com/v1", anotherClient.getBaseUrl()); } }
The above test class verifies that the ApiClient
beans are correctly registered in the application context with their respective properties (baseUrl
).
4. Conclusion
In this article, we explored the dynamic registration of beans in a Spring Boot application based on custom properties. We delved into how the BeanDefinitionRegistryPostProcessor
interface can be leveraged to achieve this dynamic bean registration, allowing for greater flexibility in application configuration. In conclusion, dynamic bean registration based on custom properties offers a powerful mechanism for configuring and managing beans in a Spring application. We can build more adaptable and configurable applications by decoupling bean definitions from the source code and allowing them to be defined dynamically.
5. Download the Source Code
This was an article on dynamic registration of Spring beans using custom properties.
You can download the full source code of this example here: spring beans dynamic registration properties