Master Dynamic Bean Switching in Spring Boot 3: 10 Real‑World Solutions
This article presents ten complete solutions for dynamically switching implementations of a Spring Boot interface—ranging from simple @Profile usage to advanced AOP and custom annotation techniques—complete with code examples, configuration snippets, and a free PDF ebook of 155 practical cases.
Introduction
In Spring Boot applications, it is common to have an interface with multiple implementations that need to be selected at runtime based on configuration, environment variables, or business rules (e.g., multi‑tenant systems, regional deployments, A/B testing). This article introduces ten comprehensive solutions covering simple configuration to advanced dynamic registration.
Practical Cases
1. Using @Profile
Separate implementations by Spring profiles so that only the bean annotated with the active profile is loaded.
public interface PaymentService {
String process();
}
public class AlipayService implements PaymentService {
public String process() { return "Alipay payment"; }
}
public class WeixinService implements PaymentService {
public String process() { return "Weixin payment"; }
} @Service
@Profile("prod")
public class AlipayService implements PaymentService {}
@Service
@Profile("test")
public class WeixinService implements PaymentService {} spring:
profiles:
active:
- prodThis approach is simple but cannot switch beans at runtime.
2. Using @ConditionalOnProperty
Load a bean based on a property value such as pack.payment.mode=alipay.
@Configuration
public class PaymentConfig {
@Bean
@ConditionalOnProperty(name = "pack.payment.mode", havingValue = "alipay")
public PaymentService alipayService() { return new AlipayService(); }
@Bean
@ConditionalOnProperty(name = "pack.payment.mode", havingValue = "weixin")
public PaymentService weixinService() { return new WeixinService(); }
}Flexible for configuration‑driven selection but still static at runtime.
3. Custom @Conditional Annotation
Define a custom condition to implement complex logic, such as region‑based selection.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(RegionCondition.class)
public @interface ConditionalOnRegion {
String value();
}
public class RegionCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String region = context.getEnvironment().getProperty("pack.payment.region");
String required = (String) metadata.getAnnotationAttributes(ConditionalOnRegion.class.getName()).get("value");
return required.equals(region);
}
} @Configuration
public class PaymentConfig {
@Bean
@ConditionalOnRegion("xinjiang")
public PaymentService alipayService() { return new AlipayService(); }
@Bean
@ConditionalOnRegion("beijing")
public PaymentService weixinService() { return new WeixinService(); }
}More complex but still does not allow runtime switching.
4. Manual Bean Registration
Programmatically register a bean using BeanDefinitionRegistry and a property that holds the class name.
@Configuration
public class DynamicBeanConfig implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private Environment env;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
.genericBeanDefinition(env.getProperty("pack.payment.className"))
.getBeanDefinition();
registry.registerBeanDefinition("paymentService", beanDefinition);
}
@Override
public void setEnvironment(Environment env) { this.env = env; }
} pack:
payment:
className: com.pack.payment.service.AlipayServiceFull control over bean creation, but the implementation cannot be switched at runtime.
5. Using @Qualifier for Static Injection
Specify the desired implementation with @Qualifier together with @Autowired.
@Service("alipay")
public class AlipayService implements PaymentService {}
@Service("weixin")
public class WeixinService implements PaymentService {}
@RestController
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(@Qualifier("weixin") PaymentService paymentService) {
this.paymentService = paymentService;
}
@GetMapping("/pay")
public void pay() { paymentService.process(); }
}Works when the implementation is known at compile time; not suitable for runtime changes.
6. Using FactoryBean
Delay bean instantiation and decide the concrete class inside getObject().
@Component
public class PaymentFactoryBean implements FactoryBean<PaymentService> {
@Value("${pack.payment.mode}")
private String mode;
@Override
public PaymentService getObject() {
return switch (mode) {
case "alipay" -> new AlipayService();
case "weixin" -> new WeixinService();
default -> null;
};
}
@Override
public Class<?> getObjectType() { return PaymentService.class; }
}Suitable for complex initialization but still creates a single instance per mode.
7. Strategy + Factory Pattern
Collect all implementations into a Map and retrieve the desired one by key at runtime.
@Component
public class PaymentFactory {
private final Map<String, PaymentService> paymentServices;
public PaymentFactory(Map<String, PaymentService> paymentServices) {
this.paymentServices = paymentServices;
}
public Optional<PaymentService> getPaymentService(String type) {
return Optional.ofNullable(paymentServices.get(type));
}
} private final PaymentFactory paymentFactory;
public PaymentController(PaymentFactory paymentFactory) { this.paymentFactory = paymentFactory; }
@GetMapping("/pay")
public String pay(String type) {
return this.paymentFactory.getPaymentService(type)
.orElseThrow(() -> new RuntimeException("Unsupported payment type [" + type + "]"))
.process();
}This is the most flexible solution and fully supports runtime switching.
8. Dynamic Proxy + AOP
Intercept method calls with an aspect and delegate to the appropriate implementation.
@Component
@Aspect
public class PaymentAspect {
private final Map<String, PaymentService> paymentServices;
public PaymentAspect(Map<String, PaymentService> paymentServices) { this.paymentServices = paymentServices; }
@Around("@annotation(usePayment)")
public Object around(ProceedingJoinPoint joinPoint, UsePayment usePayment) throws Throwable {
String type = usePayment.value();
PaymentService service = paymentServices.get(type);
if (service == null) throw new IllegalArgumentException("Unsupported payment type [" + type + "]");
Object ret = service.getClass().getMethod("process").invoke(service);
return ResponseEntity.ok(ret);
}
} @UsePayment("weixin")
@GetMapping("/pay3")
public ResponseEntity<?> pay3() { return null; }Provides a clean separation of concerns and can be extended with SpEL for more flexibility.
9. Custom Annotation + Stream
Define a custom annotation to tag implementations and select them via a stream.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PaymentType { String value(); } @Component
public class PaymentContext {
private final List<PaymentService> paymentServices;
public PaymentContext(List<PaymentService> paymentServices) { this.paymentServices = paymentServices; }
public PaymentService getPaymentService(String type) {
return paymentServices.stream()
.filter(p -> p.getClass().isAnnotationPresent(PaymentType.class))
.filter(p -> p.getClass().getAnnotation(PaymentType.class).value().equals(type))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unsupported payment type [" + type + "]"));
}
}Ideal for scenarios that require metadata‑driven selection.
10. Custom Annotation + BeanPostProcessor
A lightweight approach that uses a single custom annotation together with a BeanPostProcessor to register the appropriate bean. Detailed implementation can be found in the linked article.
All solutions are illustrated in the accompanying PDF ebook (now updated to 155 cases) which is freely available to subscribers.
How to Get the Ebook
Subscribe to the collection via the link in the article, send a private message, and the ebook will be delivered to you promptly.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
