- In Spring Boot, you can create multiple beans of the same type by providing different names for each bean. To dynamically activate or deactivate a bean, you can use a configuration property or a condition to control the bean creation based on some conditions.
- In a Spring Boot application, managing beans dynamically can be a powerful tool. One common scenario is creating two beans of the same type and toggling between them dynamically.
How Two Beans of the Same Type Work Internally 🔄
- When you define two beans of the same type in Spring, the ApplicationContext needs a way to distinguish between them. This is where the name attribute in the @Bean annotation becomes crucial.
- Each bean is assigned a unique name, and when injecting the bean into other components, you can use the @Qualifier annotation to specify which bean to inject.
@Configuration public class BeanConfig { @Bean(name = "beanA") public MyBean beanA() { return new MyBean(); } @Bean(name = "beanB") public MyBean beanB() { return new MyBean(); } }
- To inject beanA or beanB elsewhere:
@Autowired @Qualifier("beanA") private MyBean myBeanA; @Autowired @Qualifier("beanB") private MyBean myBeanB;
How Dynamic Bean Activation and Deactivation in Spring Boot work 🤔
- In Spring Boot, bean activation and deactivation dynamically typically involve managing the lifecycle of beans based on certain conditions. Here's a general overview of how it works:
- ApplicationContext:
- Spring Boot uses an ApplicationContext to manage beans. The ApplicationContext is aware of the beans defined in your application and their configurations.
- Bean Configuration:
- Beans are defined in a configuration class annotated with @Configuration. Each bean has a name, and it is registered in the ApplicationContext during the initialization process.
- Dynamic Switching:
- Dynamic activation and deactivation involve changing the reference to the active bean at runtime. This can be achieved using a service class that holds a reference to the currently active bean. When a switch is triggered (e.g., based on a request header), the service updates the reference to the active bean.
- Injection:
- Components in your application, such as controllers or services, can then inject the BeanService to access the currently active bean. This injection is often done using the @Autowired annotation.
- Conditional Logic:
- The conditions for activation and deactivation can vary. In the below provided example 1, the condition is based on the presence and value of a specific request header. However, you can design more complex conditions based on application state, external configurations, or any other dynamic factors.
Example 1 Bean-Selector
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>dynamic-bean-example</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.3</version> <!-- Use the latest Spring Boot version --> </parent> <dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- Spring Boot Starter Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency><dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.8.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.8.2</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Step 2: Create DynamicBeanExampleApplication.java
package com.example;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class DynamicBeanExampleApplication {public static void main(String[] args) {SpringApplication.run(DynamicBeanExampleApplication.class, args);}}
Step 3: Create BeanConfig.java
package com.example; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class BeanConfig { @Bean(name = "activeBean") public MyBean activeBean() { MyBean bean = new MyBean(); bean.setName("Active Bean"); return bean; } @Bean(name = "inactiveBean") public MyBean inactiveBean() { MyBean bean = new MyBean(); bean.setName("Inactive Bean"); return bean; } }
Step 4: Define BeanService.java class
package com.example; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; @Service public class BeanService { private final ApplicationContext applicationContext; private MyBean activeBean; @Autowired public BeanService(ApplicationContext applicationContext, @Qualifier("activeBean") MyBean activeBean) { this.applicationContext = applicationContext; this.activeBean = activeBean; } public MyBean getActiveBean() { return activeBean; } public void setActiveBean(String beanName) { activeBean = (MyBean) applicationContext.getBean(beanName); } }
Step 5: Define MyBean.java class
package com.example;public class MyBean { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
Step 6: Create MyController.java class
package com.example; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class MyController { private final BeanService beanService; @Autowired public MyController(BeanService beanService) { this.beanService = beanService; } @GetMapping("/my-endpoint") public String myEndpoint(@RequestHeader(name = "Bean-Selector", required = false) String beanSelector) { if (beanSelector != null && !beanSelector.isEmpty()) { beanService.setActiveBean(beanSelector); return "Bean switched based on header value: " + beanSelector; } else { return "No Bean-Selector header provided. Current active bean: " + beanService.getActiveBean().getName(); } } }
Step 7: Create MyControllerTest.java class
package com.example; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(MyController.class) public class MyControllerTest { @Autowired private MockMvc mockMvc; @Test public void myEndpoint_shouldReturnDefaultMessage() throws Exception { mockMvc.perform(get("/my-endpoint")) .andExpect(status().isOk()) .andExpect(content().string(containsString("No Bean-Selector header provided"))) .andExpect(content().string(containsString("Current active bean: Active Bean"))); } @Test public void myEndpoint_shouldSwitchToInactiveBean() throws Exception { mockMvc.perform(get("/my-endpoint") .header("Bean-Selector", "inactiveBean")) .andExpect(status().isOk()) .andExpect(content().string(containsString("Bean switched based on header value: inactiveBean"))); // Verify that the active bean is now "Inactive Bean" mockMvc.perform(get("/my-endpoint")) .andExpect(status().isOk()) .andExpect(content().string(containsString("No Bean-Selector header provided"))) .andExpect(content().string(containsString("Current active bean: Inactive Bean"))); } @Test public void myEndpoint_shouldSwitchToActiveBean() throws Exception { // Switch to inactive bean first mockMvc.perform(get("/my-endpoint") .header("Bean-Selector", "inactiveBean")) .andExpect(status().isOk()) .andExpect(content().string(containsString("Bean switched based on header value: inactiveBean"))); // Switch back to active bean mockMvc.perform(get("/my-endpoint") .header("Bean-Selector", "activeBean")) .andExpect(status().isOk()) .andExpect(content().string(containsString("Bean switched based on header value: activeBean"))); // Verify that the active bean is now "Active Bean" mockMvc.perform(get("/my-endpoint")) .andExpect(status().isOk()) .andExpect(content().string(containsString("No Bean-Selector header provided"))) .andExpect(content().string(containsString("Current active bean: Active Bean"))); } }
How Two Beans of the Same Type Activate and Deactivate Dynamically Internally
- In the provided example of dynamic activation and deactivation, two beans of the same type are defined with names "activeBean" and "inactiveBean". The BeanService class manages the switching logic:
- During application startup, the BeanService is initialized with a default active bean (in this case, "activeBean").
- When the /toggle-bean endpoint is called (e.g., through an HTTP request), the BeanService dynamically switches between the active and inactive beans.
- Internally, the applicationContext.getBean(beanName) method is used to retrieve the bean instance based on its name.
- The controller (MyController) injects the BeanService and can access the currently active bean.
- By using the @RequestHeader annotation, the controller can receive a header value that determines which bean should be active. This header value is then used to switch between beans dynamically.
- In summary, the key is to have a mechanism to switch the active bean dynamically, and in this example, it's triggered by a request header. The ApplicationContext and the @Qualifier annotation help distinguish between two beans of the same type
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>DynamicBeanSwitcher</artifactId> <version>1.0.0</version> <properties> <java.version>11</java.version> <spring-boot.version>2.6.1</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency><dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.8.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.8.2</version> <scope>test</scope> </dependency> <!-- Add other dependencies as needed --> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Step 2 Create MyController.java class
@RestController public class MyController { @Autowired private DynamicBeanSwitcher beanSwitcher; @GetMapping("/active-bean") public YourBeanType getActiveBean() { return beanSwitcher.getActiveBean(); } }
Step 3 Create MySpringBootApplication.java class
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MySpringBootApplication { public static void main(String[] args) { SpringApplication.run(MySpringBootApplication.class, args); } }
Step 4 Create YourBeanType.java class
public class YourBeanType { // Your bean implementation }
Step 5 Create DynamicBeanSwitcher.java class
- Fields:
- YourBeanType beanA: Field to hold an instance of the beanA.
- YourBeanType beanB: Field to hold an instance of the beanB.
- DynamicBeanConfiguration.BeanProperties beanProperties: Field to hold the configuration properties used for deciding which bean is active.
- Constructor:
- The constructor takes three parameters:
- YourBeanType beanA: Injected instance of beanA.
- YourBeanType beanB: Injected instance of beanB.
- DynamicBeanConfiguration.BeanProperties beanProperties: Injected configuration properties.
- getActiveBean Method:
- This method determines and returns the active bean based on the configuration properties.
- If beanAEnabled is true, it returns beanA.
- If beanBEnabled is true, it returns beanB.
- If neither is enabled, it throws a RuntimeException with a message indicating that no active bean is found.
- The purpose of this class is to encapsulate the logic of selecting the active bean based on the configuration.
- It is typically used in other components of the application that need to interact with the currently active bean.
package com.example.config; import org.springframework.beans.factory.annotation.Qualifier; public class DynamicBeanSwitcher { private YourBeanType beanA; private YourBeanType beanB; private DynamicBeanConfiguration.BeanProperties beanProperties; public DynamicBeanSwitcher( @Qualifier("beanA") YourBeanType beanA, @Qualifier("beanB") YourBeanType beanB, DynamicBeanConfiguration.BeanProperties beanProperties) { this.beanA = beanA; this.beanB = beanB; this.beanProperties = beanProperties; } public YourBeanType getActiveBean() { if (beanProperties.isBeanAEnabled()) { return beanA; } else if (beanProperties.isBeanBEnabled()) { return beanB; } else { // Handle default case or throw an exception throw new RuntimeException("No active bean found."); } } }
Step 6 Create DynamicBeanConfiguration.java class
- Configuration Annotation:
- @Configuration: Indicates that this class contains bean definitions and should be processed by the Spring container.
- Bean Definitions:
- @Bean("beanA") and @Bean("beanB"): Define beans named "beanA" and "beanB" respectively.
- @Conditional(BeanACondition.class) and @Conditional(BeanBCondition.class): Specify conditions for creating beanA and beanB, respectively. These conditions are defined by BeanACondition and BeanBCondition classes.
- DynamicBeanSwitcher Bean Definition:
- @Bean: Defines the DynamicBeanSwitcher bean, injecting instances of beanA, beanB, and BeanProperties.
- BeanProperties Class:
- @ConfigurationProperties(prefix = "yourapp.beans"): Binds properties with the prefix "yourapp.beans" to the BeanProperties class. This class holds configuration properties for enabling/disabling beanA and beanB.
- BeanACondition and BeanBCondition Classes:
- BeanACondition and BeanBCondition are classes implementing the Condition interface.
- They are conditions for creating beanA and beanB respectively.
- They are injected with BeanProperties to determine if the corresponding bean should be enabled based on configuration properties.
- The purpose of this configuration class is to define beans (beanA and beanB) conditionally based on certain criteria (BeanACondition and BeanBCondition).
- The DynamicBeanSwitcher bean is then defined, which uses these conditional beans based on the configuration properties provided by the BeanProperties class.
package com.example.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; @Configuration public class DynamicBeanConfiguration { @Bean("beanA") @Conditional(BeanACondition.class) public YourBeanType beanA() { return new YourBeanType("BeanA", 42); // Example instantiation, customize as needed } @Bean("beanB") @Conditional(BeanBCondition.class) public YourBeanType beanB() { return new YourBeanType("BeanB", 123); // Example instantiation, customize as needed } @Bean public DynamicBeanSwitcher dynamicBeanSwitcher( @Qualifier("beanA") YourBeanType beanA, @Qualifier("beanB") YourBeanType beanB, BeanProperties beanProperties) { return new DynamicBeanSwitcher(beanA, beanB, beanProperties); } @ConfigurationProperties(prefix = "yourapp.beans") public static class BeanProperties { private boolean beanAEnabled; private boolean beanBEnabled; public boolean isBeanAEnabled() { return beanAEnabled; } public void setBeanAEnabled(boolean beanAEnabled) { this.beanAEnabled = beanAEnabled; } public boolean isBeanBEnabled() { return beanBEnabled; } public void setBeanBEnabled(boolean beanBEnabled) { this.beanBEnabled = beanBEnabled; } } public static class BeanACondition implements Condition { @Autowired private BeanProperties beanProperties; @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return beanProperties.isBeanAEnabled(); } } public static class BeanBCondition implements Condition { @Autowired private BeanProperties beanProperties; @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return beanProperties.isBeanBEnabled(); } } }
Step 7 Define application.properties
yourapp.beans.beanAEnabled=true yourapp.beans.beanBEnabled=false
Step 7 Create MySpringBootApplicationTests.java class
package com.example; import com.example.config.DynamicBeanConfiguration; import com.example.config.DynamicBeanSwitcher; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ContextConfiguration; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringBootTest @ContextConfiguration(classes = DynamicBeanConfiguration.class) class MySpringBootApplicationTests { @Autowired private DynamicBeanSwitcher dynamicBeanSwitcher; @Autowired private YourBeanType beanA; @Autowired private YourBeanType beanB; @Test void contextLoads() { assertNotNull(dynamicBeanSwitcher); assertNotNull(beanA); assertNotNull(beanB); // Test that the active bean matches the expected bean YourBeanType activeBean = dynamicBeanSwitcher.getActiveBean(); assertEquals(beanA, activeBean); // Assuming beanA is expected to be active in this configuration } }
Example 3 Dynamic-beans-demo with BeanPostProcessor
- This example demonstrates the creation of multiple beans dynamically based on the configuration specified in the application.properties file. Additionally, it utilizes a BeanPostProcessor for customization
Step 1 Create DynamicBeansDemoApplication.java
package com.example.dynamicbeans; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DynamicBeansDemoApplication { public static void main(String[] args) { SpringApplication.run(DynamicBeansDemoApplication.class, args); } }
Step 2 Create BeanConfig.java
package com.example.dynamicbeans.config; import com.example.dynamicbeans.bean.DynamicBean; import com.example.dynamicbeans.postprocessor.BeanPostProcessorCustomizer; import com.example.dynamicbeans.service.DynamicBeanFactory; import com.example.dynamicbeans.service.MyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class BeanConfig { @Autowired private DynamicBeansProperties dynamicBeansProperties; @Bean public BeanPostProcessorCustomizer beanPostProcessorCustomizer() { return new BeanPostProcessorCustomizer(); } @Bean public DynamicBeanFactory dynamicBeanFactory() { return new DynamicBeanFactory(dynamicBeansProperties.getNames()); } @Bean public MyService myService(DynamicBeanFactory dynamicBeanFactory) { return new MyService(dynamicBeanFactory.createDynamicBeans()); } }
Step 3 Create DynamicBeanFactory.java
package com.example.dynamicbeans.service; import com.example.dynamicbeans.bean.DynamicBean; import java.util.ArrayList; import java.util.List; public class DynamicBeanFactory { private List<String> dynamicBeanNames; public DynamicBeanFactory(List<String> dynamicBeanNames) { this.dynamicBeanNames = dynamicBeanNames; } public List<DynamicBean> createDynamicBeans() { List<DynamicBean> dynamicBeans = new ArrayList<>(); for (String beanName : dynamicBeanNames) { dynamicBeans.add(new DynamicBean(beanName)); } return dynamicBeans; } }
Step 4 Define DynamicBean.java
package com.example.dynamicbeans.bean; public class DynamicBean { private String name; public DynamicBean(String name) { this.name = name; } public void doSomething() { System.out.println("Doing something with " + name); } }
Step 5 Define DynamicBeansProperties.java
package com.example.dynamicbeans.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import java.util.List; @Configuration @ConfigurationProperties(prefix = "dynamic.beans") public class DynamicBeansProperties { private List<String> names; private boolean activate; // getters and setters public List<String> getNames() { return names; } public void setNames(List<String> names) { this.names = names; } public boolean isActivate() { return activate; } public void setActivate(boolean activate) { this.activate = activate; } }
Step 6 Define MyService.java
package com.example.dynamicbeans.service; import com.example.dynamicbeans.bean.DynamicBean; import java.util.List; public class MyService { private final List<DynamicBean> dynamicBeans; public MyService(List<DynamicBean> dynamicBeans) { this.dynamicBeans = dynamicBeans; } public void performOperations() { dynamicBeans.forEach(DynamicBean::doSomething); } }
Step 7 Create BeanPostProcessorCustomizer.java
package com.example.dynamicbeans.postprocessor; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; @Component public class BeanPostProcessorCustomizer implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("Before Initialization: " + beanName); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("After Initialization: " + beanName); return bean; } }
Step 7 Define application.properties
dynamic.beans.names=Bean1,Bean2,Bean3 dynamic.beans.activate=true
Conclusion
- In conclusion, dynamic bean activation and deactivation enhance the adaptability of Spring Boot applications by allowing the runtime configuration to dictate which beans are active.
- This approach facilitates a more modular and configurable architecture, enabling developers to respond effectively to changing requirements without significant code modifications.
- Integrating such dynamic features ensures that the application remains agile and easily adjustable to different scenarios.
Other Reference Links