Backend Development 9 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Dynamically Enable/Disable Spring Boot Controllers with AOP, Interceptors, and Custom Mappings

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.

AOPbackend developmentfeature-toggleSpring BootInterceptorCustom Mapping
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.