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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Dynamic Bean Switching in Spring Boot 3: 10 Real‑World Solutions

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:
    - prod

This 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.AlipayService

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

图片
图片
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring Bootdependency-injectionDynamic Bean Switching
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.