You need to route requests to different controller methods based on a custom rule (e.g. an API version header) that plain @RequestMapping can't express. How would you extend the HandlerMapping / RequestMappingInfo machinery to do this cleanly?
answer
- RequestCondition: combine / getMatchingCondition / compareTo
- override getCustomMethodCondition + getCustomTypeCondition
- replace default via WebMvcRegistrations (stay at order 0)
- null match = reject; compareTo ranks specificity
- Spring 6.2 has native @RequestMapping(version=...)
basics
~20 sSubclass RequestMappingHandlerMapping and override getCustomTypeCondition/getCustomMethodCondition to attach your own RequestCondition (e.g. one that matches an API-Version header). Spring folds it into each RequestMappingInfo, so it participates in matching and specificity ranking alongside the built-in conditions.
solid answer
~40 sThe extension point is a custom RequestCondition plugged into RequestMappingInfo. Subclass RequestMappingHandlerMapping and override getCustomMethodCondition (and/or getCustomTypeCondition) to return your condition for methods/classes carrying, say, an @ApiVersion annotation. Implement RequestCondition<T> with getMatchingCondition(request) (returns null = no match), combine(other) for class+method merging, and compareTo(other, request) so more specific versions win during selection. Register the subclass as the primary mapping — in Boot, via a WebMvcRegistrations bean returning it from getRequestMappingHandlerMapping — so it replaces the default at order 0. Your condition then becomes a first-class part of matching, 405/406 signaling, and ambiguity resolution, rather than being bolted on with brittle if-checks inside controllers.
code
java · 35 lines@Retention(RetentionPolicy.RUNTIME) @Target(METHOD)
@interface ApiVersion { int value(); }
class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private final int version;
ApiVersionCondition(int v) { this.version = v; }
// method-level wins over type-level
public ApiVersionCondition combine(ApiVersionCondition other) { return other; }
// match if the request's requested version >= this method's version
public ApiVersionCondition getMatchingCondition(HttpServletRequest req) {
int requested = parseVersionHeader(req); // e.g. from X-API-Version
return requested >= version ? this : null; // null => no match
}
// higher declared version = more specific = sorts first
public int compareTo(ApiVersionCondition other, HttpServletRequest req) {
return Integer.compare(other.version, this.version);
}
private int parseVersionHeader(HttpServletRequest r) { /* ... */ return 1; }
}
class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
@Override protected RequestCondition<?> getCustomMethodCondition(Method method) {
ApiVersion a = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return a != null ? new ApiVersionCondition(a.value()) : null;
}
}
@Configuration
class Cfg implements WebMvcRegistrations {
@Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping();
}
}go deeper
Likely only knows headers/params attributes on @RequestMapping — acceptable to stop there.
Should reach for @RequestMapping(headers=...) or content-negotiation and know their limits.
Knows a custom RequestCondition exists and roughly how getCustomMethodCondition hooks in.
Designs the full RequestCondition (combine/getMatchingCondition/compareTo), replaces the default via WebMvcRegistrations, weighs it against native 6.2 versioning and content negotiation, and anticipates ambiguity pitfalls.
## Why not just use params/headers on @RequestMapping `@RequestMapping(headers = "X-API-Version=2")` works for trivial cases, but it can't express ranges ('>= 2'), fallbacks, or semantic version comparison, and it scatters version literals across annotations. The clean solution is to teach the *mapping machinery* about versions so they behave like a native condition — matched, ranked, and error-signaled like path or method. ## The RequestCondition abstraction `org.springframework.web.servlet.mvc.condition.RequestCondition<T>` is the interface every matching facet implements (patterns, methods, params, headers, consumes, produces all do). It has three responsibilities: 1. `T combine(T other)` — merge a type-level condition with a method-level one (class `@RequestMapping` + method `@GetMapping`). 2. `T getMatchingCondition(HttpServletRequest request)` — return a narrowed condition if the request matches, or `null` if it doesn't (null propagates to 'this RequestMappingInfo doesn't match'). 3. `int compareTo(T other, HttpServletRequest request)` — order two matches by specificity so the best wins. ## Wiring a custom condition into RequestMappingInfo `RequestMappingHandlerMapping` exposes two protected hooks: - `RequestCondition<?> getCustomTypeCondition(Class<?> handlerType)` - `RequestCondition<?> getCustomMethodCondition(Method method)` Override them to inspect your annotation (e.g. `@ApiVersion`) and return your condition. Spring stores it in `RequestMappingInfo` as the *custom* condition; it then automatically participates in `getMatchingCondition` (AND-ed with all built-ins) and in the specificity comparator. ## Registering the subclass in Spring Boot You must *replace* the default RequestMappingHandlerMapping, not add a second one. In Boot the sanctioned way is a `WebMvcRegistrations` bean: ```java @Configuration class ApiVersionConfig implements WebMvcRegistrations { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new ApiVersionRequestMappingHandlerMapping(); } } ``` This keeps it at order 0 and preserves all the auto-configured settings. In plain Spring MVC you override `WebMvcConfigurationSupport.createRequestMappingHandlerMapping()` (via `DelegatingWebMvcConfiguration`). ## Native Spring API versioning (6.2+) Spring Framework 6.2 added *built-in* API versioning: `@RequestMapping(version = "1.2")` plus `configureApiVersioning` in `WebMvcConfigurer`, backed by an internal `VersionRequestCondition`. On modern stacks prefer this over a hand-rolled condition; the custom-condition approach remains the answer on older versions or for bespoke rules the built-in doesn't cover. (State the version awareness — it signals current knowledge.) ## Alternatives and when to choose them - **Custom RequestCondition** — best when routing truly depends on the rule and you want proper specificity/405 semantics. - **A dedicated custom HandlerMapping** (subclass `AbstractHandlerMethodMapping`) — heavier; only when you're not mapping via `@RequestMapping` at all. - **HandlerInterceptor / filter** — wrong tool for *selecting* a handler; fine for cross-cutting concerns after selection. - **Content negotiation (`produces`)** — the idiomatic route for media-type versioning (`application/vnd.app.v2+json`) without custom code. ## Gotchas - Registering a *second* RequestMappingHandlerMapping instead of replacing the default causes duplicate mappings and confusing precedence — always replace via `WebMvcRegistrations`. - `getMatchingCondition` returning a non-null but empty condition vs null changes match semantics — return null to reject. - `compareTo` must be consistent and antisymmetric or you'll get flaky selection / spurious 'Ambiguous mapping' errors. - Your condition must be immutable and cheap — it runs on every matching request.
- Why replace the default RequestMappingHandlerMapping via WebMvcRegistrations instead of just declaring your subclass as an extra @Bean?Declaring an extra bean adds a SECOND mapping at a different order, producing duplicate/competing mappings and losing the auto-configured settings. WebMvcRegistrations swaps the instance Spring Boot itself creates, keeping order 0 and all default configuration intact.
- What role does the condition's compareTo play, and what breaks if it's wrong?compareTo ranks two matching RequestMappingInfos so the most specific version wins. If it's inconsistent (not antisymmetric/transitive) selection becomes nondeterministic or Spring throws 'Ambiguous handler methods' because it can't decide a best match.
saying these in an interview costs you the question
- Proposing an interceptor/filter to SELECT the handler (they run after selection)
- Adding a second RequestMappingHandlerMapping bean instead of replacing the default
- Putting version if/else logic inside controller methods and calling that 'routing'
- Forgetting compareTo, so specificity/ambiguity resolution breaks
- Returning an empty condition instead of null to signal no-match