How to Dynamically Enable/Disable Spring Boot Controllers with AOP, Interceptors, and Custom Mappings
This article explains four practical ways to control the availability of Spring Boot controller endpoints at runtime—using @ConditionalOnProperty, a custom AOP annotation, a HandlerInterceptor, and a custom RequestMappingHandlerMapping—detailing code examples, configuration, advantages, and trade‑offs.
1. Introduction
Dynamic control of API availability is common for gray releases, feature toggles, emergency circuit breaking, and similar scenarios. Spring Boot provides several flexible methods to switch controllers on or off, both with AOP and non‑AOP approaches, each offering different granularity and runtime characteristics.
2. Practical Cases
2.1 Solution 1: @ConditionalOnProperty
Uses Spring Boot's conditional bean registration to decide at startup whether a controller is created based on a property such as pack.features.users.enabled . Example:
<code>@RestController
@ConditionalOnProperty(name = "pack.features.users.enabled", havingValue = "true")
public class UserController {
}
</code>Advantages: zero code, native support, no runtime overhead. Disadvantages: requires restart, coarse granularity (class/bean level).
2.2 Solution 2: AOP with custom annotation
Define a @FeatureToggle annotation and an aspect that checks the corresponding property on each request, throwing an exception when disabled.
<code>@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeatureToggle {
/** Feature name used to look up configuration */
String featureName();
/** Message returned when disabled */
String defaultError() default "Service unavailable";
}
</code> <code>@Aspect
@Component
public class FeatureToggleAspect {
private static final String PREFIX = "pack.features.";
private static final String SUFFIX = ".enabled";
private final Environment env;
public FeatureToggleAspect(Environment env) { this.env = env; }
@Pointcut("@within(toggle) || @annotation(toggle)")
public void matchAnnotatedClassOrMethod(FeatureToggle toggle) {}
@Around("matchAnnotatedClassOrMethod(toggle)")
public Object checkFeature(ProceedingJoinPoint pjp, FeatureToggle toggle) throws Throwable {
if (toggle == null) {
toggle = AopUtils.getTargetClass(pjp.getTarget()).getAnnotation(FeatureToggle.class);
}
if (toggle == null) {
return pjp.proceed();
}
String key = PREFIX + toggle.featureName() + SUFFIX;
boolean enabled = Boolean.parseBoolean(env.getProperty(key, "false"));
if (!enabled) {
throw new RuntimeException(toggle.defaultError());
}
return pjp.proceed();
}
}
</code>Usage example:
<code>@RestController
@RequestMapping("/users")
@FeatureToggle(featureName = "user")
public class UserController {
@FeatureToggle(featureName = "user.query")
@GetMapping("/query")
public ResponseEntity<?> query() { return ResponseEntity.ok("query..."); }
@GetMapping("/create")
public ResponseEntity<?> create() { return ResponseEntity.ok("create..."); }
}
</code>Configuration (application.yml):
<code>pack:
features:
user:
enabled: false
user.query:
enabled: true
</code>2.3 Solution 3: HandlerInterceptor
Implement HandlerInterceptor to check the toggle before the controller is invoked.
<code>@Component
public class FeatureToggleInterceptor implements HandlerInterceptor {
private final FeatureProperties featureProperties;
public FeatureToggleInterceptor(FeatureProperties featureProperties) {
this.featureProperties = featureProperties;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
FeatureToggle toggle = hm.getMethodAnnotation(FeatureToggle.class);
if (toggle == null) {
toggle = hm.getBeanType().getAnnotation(FeatureToggle.class);
}
if (toggle != null && !featureProperties.isEnabled(toggle.featureName())) {
throw new RuntimeException(toggle.defaultError());
}
}
return true;
}
}
</code>Register the interceptor in WebMvcConfigurer :
<code>@Component
public class WebConfig implements WebMvcConfigurer {
private final FeatureToggleInterceptor toggleInterceptor;
public WebConfig(FeatureToggleInterceptor toggleInterceptor) {
this.toggleInterceptor = toggleInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(toggleInterceptor).addPathPatterns("/**");
}
}
</code>Advantages: avoids AOP overhead, suitable for URL‑level control.
2.4 Solution 4: Custom RequestMappingHandlerMapping
Extend RequestMappingHandlerMapping to filter handlers during the routing phase.
<code>@Component
public class PackHandlerMapping extends RequestMappingHandlerMapping {
private final FeatureProperties featureProperties;
public PackHandlerMapping(FeatureProperties featureProperties) {
this.featureProperties = featureProperties;
}
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
HandlerMethod handler = super.getHandlerInternal(request);
if (handler != null) {
FeatureToggle toggle = handler.getMethodAnnotation(FeatureToggle.class);
if (toggle == null) {
toggle = handler.getBeanType().getAnnotation(FeatureToggle.class);
}
if (toggle != null && !featureProperties.isEnabled(toggle.featureName())) {
return null;
}
}
return handler;
}
}
</code>Register it via WebMvcRegistrations :
<code>@Component
public class HandlerConfig implements WebMvcRegistrations {
private final FeatureProperties featureProperties;
public HandlerConfig(FeatureProperties featureProperties) {
this.featureProperties = featureProperties;
}
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new PackHandlerMapping(featureProperties);
}
}
</code>This approach provides the best performance because the check happens at the earliest routing stage.
All four solutions achieve the same effect: the controller can be turned on or off dynamically according to configuration, with trade‑offs in granularity, restart requirement, and runtime overhead.
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.