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