Maria Gomez, a favorite colleague, asked a wonderful question, "How can I have feature toggles on Spring MVC controller request handler methods?" Existing Java feature toggle libraries focus on toggling individual beans, or using if/else logic inside methods, and don't work at the method level.
Given a trivial example toggle:
@Documented @Retention(RUNTIME) @Target(METHOD) public @interface Enabled { boolean value(); }
I'd like my controller to work like this:
@RestController @RequestMapping(PATH) public class HelloWorldController { public static final String PATH = "/hello-world"; private static final String texanTemplate = "Howdy, %s!"; private static final String russianTemplate = "Привет, %s!"; private final AtomicLong counter = new AtomicLong(); @Enabled(true) @RequestMapping(value = "/{name}", method = GET) public Greeting sayHowdy(@PathVariable("name") final String name) { return new Greeting(counter.incrementAndGet(), format(texanTemplate, name)); } @Enabled(false) @RequestMapping(value = "/{name}", method = GET) public Greeting sayPrivet(@PathVariable("name") final String name) { return new Greeting(counter.incrementAndGet(), format(russianTemplate, name)); } }
(Greeting
is a simple struct turned into JSON by Spring.)
To make the example a little more sophisticated, I'd like to use a "3rd-party library" to decide on which features to activate (think "Togglz" or "FF4J", say):
@Component public class EnabledChecker { public boolean isMapped(final Enabled enabled) { return null == enabled || enabled.value(); } }
Originally I investigated Spring's RequestCondition
classes, thinking I could do the same as @RequestMapping(... match conditions ...)
. However, this is tricky! Spring uses these conditions to decide which method to invoke for each HTTP request, not when deciding which methods should be treated as the handler for a given HTTP path. Taking this route, Spring complains at wiring time of duplicate handlers for the same request path.
The right way is to control the initial wiring of request handler methods, not decide later. First extend RequestMappingHandlerMapping
(what a mouthful!):
public class EnabledRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Autowired private EnabledChecker checker; @Override protected RequestMappingInfo getMappingForMethod(final Method method, final Class<?> handlerType) { final Enabled enabled = findAnnotation(method, Enabled.class); final boolean mapped = checker.isMapped(enabled); return mapped ? super.getMappingForMethod(method, handlerType) : null; } }
Note this is not directly a bean (no @Component
). We need one more bit, to override the factory method that creates these handler mappings:
@Configuration public class EnabledWebMvcConfigurationSupport extends WebMvcConfigurationSupport { @Override protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() { return new EnabledRequestMappingHandlerMapping(); } }
And Bob's your uncle. EnabledWebMvcConfigurationSupport
ensures the returned ReqeustMappingHandlerMapping
is injected, and so the "3rd-party library" is available to consult.
Full code in Github.
No comments:
Post a Comment