Backend Development 9 min read

How to Dynamically Add Switch Controls to Spring Boot APIs with AOP

This guide explains how to implement a dynamic feature‑toggle for Spring Boot API endpoints using custom annotations, resolver interfaces, and an AOP aspect, enabling flexible control, safety, logging, monitoring, and user‑friendly fallback responses.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Dynamically Add Switch Controls to Spring Boot APIs with AOP

Overview

This article shows how to add a dynamic switch to API endpoints so that their normal access can be controlled or a friendly message displayed. The switch improves flexibility, safety, error handling, monitoring, user experience, and compliance during development and maintenance.

Why add an API switch?

Flexibility and extensibility : Dynamically control API behavior without code changes.

Security and control : Prevent unauthorized access in testing, maintenance, or sensitive data scenarios.

Error handling and logging : Use the switch to aid troubleshooting and system optimization.

System monitoring and management : Track switch state changes to understand usage and performance.

User experience : Show friendly messages when an API is unavailable.

Compliance and privacy : Ensure regulations are respected for sensitive APIs.

Implementation plan

Define an AOP aspect that intercepts controller methods and decides, based on a switch key, whether to proceed with the original method or invoke a fallback.

Custom annotation

<code>@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ApiSwitch {
    /**接口对应的key,通过可以该key查询接口是否关闭*/
    String key() default "";
    /**解析器beanName,通过具体的实现获取key对应的值*/
    String resolver() default "";
    /**开启后降级方法名*/
    String fallback() default "";
}</code>

Resolver interface

<code>public interface SwitchResolver {
    boolean resolver(String key);
    void config(String key, Integer onoff);
}</code>

Default implementations

In‑memory map implementation:

<code>@Component
public class ConcurrentMapResolver implements SwitchResolver {
    private Map<String, Integer> keys = new ConcurrentHashMap<>();

    @Override
    public boolean resolver(String key) {
        Integer value = keys.get(key);
        return value == null ? false : (value == 1);
    }

    public void config(String key, Integer onoff) {
        keys.put(key, onoff);
    }
}</code>

Redis‑backed implementation:

<code>@Component
public class RedisResolver implements SwitchResolver {
    private final StringRedisTemplate stringRedisTemplate;

    public RedisResolver(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean resolver(String key) {
        String value = this.stringRedisTemplate.opsForValue().get(key);
        return !(value == null || "0".equals(value));
    }

    @Override
    public void config(String key, Integer onoff) {
        this.stringRedisTemplate.opsForValue().set(key, String.valueOf(onoff));
    }
}</code>

Aspect definition

<code>@Component
@Aspect
public class ApiSwitchAspect implements ApplicationContextAware {
    private ApplicationContext context;
    private final SwitchProperties switchProperties;
    public static final Map<String, Class<? extends SwitchResolver>> MAPPINGS;
    static {
        Map<String, Class<? extends SwitchResolver>> mappings = new HashMap<>();
        mappings.put("map", ConcurrentMapResolver.class);
        mappings.put("redis", RedisResolver.class);
        MAPPINGS = Collections.unmodifiableMap(mappings);
    }
    public ApiSwitchAspect(SwitchProperties switchProperties) {
        this.switchProperties = switchProperties;
    }
    @Pointcut("@annotation(apiSwitch)")
    private void onoff(ApiSwitch apiSwitch) {}
    @Around("onoff(apiSwitch)")
    public Object ctl(ProceedingJoinPoint pjp, ApiSwitch apiSwitch) throws Throwable {
        String key = apiSwitch.key();
        String resolverName = apiSwitch.resolver();
        String fallback = apiSwitch.fallback();
        SwitchResolver resolver = null;
        if (StringUtils.hasLength(resolverName)) {
            resolver = this.context.getBean(resolverName, SwitchResolver.class);
        } else {
            resolver = this.context.getBean(MAPPINGS.get(this.switchProperties.getResolver()));
        }
        if (resolver == null || !resolver.resolver(key)) {
            return pjp.proceed();
        }
        if (!StringUtils.hasLength(fallback)) {
            return "接口不可用";
        }
        Class<?> clazz = pjp.getSignature().getDeclaringType();
        Method fallbackMethod = clazz.getDeclaredMethod(fallback);
        return fallbackMethod.invoke(pjp.getTarget());
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }
}</code>

Switch control endpoint

<code>@GetMapping("/onoff/{state}")
public Object onoff(String key, @PathVariable("state") Integer state) {
    String resolverType = switchProperties.getResolver();
    if (!StringUtils.hasLength(resolverType)) {
        SwitchResolver bean = this.context.getBean(ApiSwitchAspect.MAPPINGS.get("map"));
        if (bean instanceof ConcurrentMapResolver resolver) {
            resolver.config(key, state);
        }
    } else {
        SwitchResolver resolver = this.context.getBean(ApiSwitchAspect.MAPPINGS.get(resolverType));
        resolver.config(key, state);
    }
    return "success";
}</code>

Test example

<code>@GetMapping("/q1")
@ApiSwitch(key = "swtich$q1", fallback = "q1_fallback", resolver = "redisResolver")
public Object q1() {
    return "q1";
}

public Object q1_fallback() {
    return "接口维护中";
}</code>

The configuration only requires the key ; other attributes are optional. After deploying, you can toggle the switch via the /onoff/{state} endpoint using either the in‑memory map or Redis implementation.

Conclusion

Using Spring AOP to implement API switches provides precise interception, flexible control, and easy fallback handling, which enhances maintainability, extensibility, and overall system robustness.

backendAOPfeature-toggleSpring Bootapi-switch
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

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