Thursday, November 16, 2023

Empowering Flexibility: Dynamic Bean Activation and Deactivation in Spring Boot

  • 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.
Example:
@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 Structure 

Step 1: Define the pom.xml

<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;

@SpringBootApplication
public 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

Example 2 Dynamic Bean Switcher

  • Project Structure 
  •   

Step 1: Define  pom.xml

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

    You may also like

    Kubernetes Microservices
    Python AI/ML
    Spring Framework Spring Boot
    Core Java Java Coding Question
    Maven AWS