How do HandlerMethodArgumentResolver and HandlerMethodReturnValueHandler work, and how does the adapter pick the right one?
answer
- supportsParameter / resolveArgument
- supportsReturnType / handleReturnValue
- composite = first match wins, cached
- custom resolvers appended AFTER built-ins
- RequestResponseBodyMethodProcessor implements both SPIs
basics
~20 sFor each parameter the adapter asks every argument resolver supportsParameter; the first match resolves the value. For the return value it asks every return-value handler supportsReturnType; the first match writes the response or sets the view. Both are strategy lists tried in order.
solid answer
~30 sRequestMappingHandlerAdapter holds two ordered strategy lists wrapped in composites: HandlerMethodArgumentResolverComposite and HandlerMethodReturnValueHandlerComposite. Argument resolution: for each MethodParameter, ServletInvocableHandlerMethod loops the resolvers calling supportsParameter; the first true resolver's resolveArgument produces the value (e.g. RequestParamMethodArgumentResolver for @RequestParam, PathVariableMethodArgumentResolver for @PathVariable, RequestResponseBodyMethodProcessor for @RequestBody using HttpMessageConverters). The composite caches which resolver matched each parameter. Return handling mirrors this: handleReturnValue loops return-value handlers calling supportsReturnType; the first match (e.g. RequestResponseBodyMethodProcessor for @ResponseBody, ViewNameMethodReturnValueHandler for a String, HttpEntityMethodProcessor for ResponseEntity) processes it. Order matters because some handlers are broad. You extend either list via WebMvcConfigurer.addArgumentResolvers / addReturnValueHandlers, which append after (not before) the defaults.
code
java · 22 lines// Inject the authenticated user as a typed parameter via a custom resolver.
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override public boolean supportsParameter(MethodParameter p) {
return p.hasParameterAnnotation(CurrentUser.class)
&& p.getParameterType().equals(AppUser.class);
}
@Override public Object resolveArgument(MethodParameter p, ModelAndViewContainer mav,
NativeWebRequest req, WebDataBinderFactory bf) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth == null ? null : ((AppPrincipal) auth.getPrincipal()).toAppUser();
}
}
@Configuration
class WebConfig implements WebMvcConfigurer {
@Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserArgumentResolver());
}
}
// Usage:
// @GetMapping("/me") AppUser me(@CurrentUser AppUser user) { return user; }go deeper
Know parameters are filled by argument resolvers and return values handled by return-value handlers, chosen by annotation/type.
Name the two SPIs (supportsParameter/resolveArgument, supportsReturnType/handleReturnValue) and give examples per annotation.
Explain the composites, first-match ordering + caching, and writing/registering a custom argument resolver.
Discuss the append-after-defaults ordering limitation, when to prefer a custom HttpMessageConverter over a return-value handler, and override strategies.
## Two strategy interfaces Spring turns the messy job of 'fill this method's parameters' and 'turn this return value into a response' into two Strategy-pattern SPIs. ### HandlerMethodArgumentResolver ```java public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception; } ``` Each implementation typically keys off an annotation or parameter type. Notable built-ins: - `RequestParamMethodArgumentResolver` — `@RequestParam`, and simple types without an annotation. - `PathVariableMethodArgumentResolver` — `@PathVariable`. - `RequestResponseBodyMethodProcessor` — `@RequestBody` (runs `HttpMessageConverter`s) AND `@ResponseBody` returns (it implements *both* SPIs). - `ModelAttributeMethodProcessor` — `@ModelAttribute` / model binding. - `ServletRequestMethodArgumentResolver` — `HttpServletRequest`, `Locale`, `Principal`, etc. - `PageableHandlerMethodArgumentResolver` (Spring Data) — `Pageable`. ### HandlerMethodReturnValueHandler ```java public interface HandlerMethodReturnValueHandler { boolean supportsReturnType(MethodParameter returnType); void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception; } ``` Notable built-ins: - `RequestResponseBodyMethodProcessor` — `@ResponseBody`; serializes via converters, sets `mavContainer.setRequestHandled(true)`. - `HttpEntityMethodProcessor` — `ResponseEntity`/`HttpEntity`. - `ViewNameMethodReturnValueHandler` — `String` view name. - `ModelAndViewMethodReturnValueHandler` — an explicit `ModelAndView`. - `ModelMethodProcessor`, `ViewMethodReturnValueHandler`, `CallableMethodReturnValueHandler`, `DeferredResultMethodReturnValueHandler`, `StreamingResponseBodyReturnValueHandler`. ## The composites Both lists are wrapped in a **Composite** that itself implements the SPI: - `HandlerMethodArgumentResolverComposite.supportsParameter` returns true if *any* delegate supports it; `resolveArgument` finds the matching delegate (and caches the mapping in a `ConcurrentHashMap` keyed by `MethodParameter` to avoid re-scanning on every request). - `HandlerMethodReturnValueHandlerComposite` does the same for return values. **First match wins**, so ordering is significant. `RequestMappingHandlerAdapter` assembles the defaults in `getDefaultArgumentResolvers()` / `getDefaultReturnValueHandlers()`, deliberately placing annotation-based resolvers before the catch-all ones, and custom resolvers in a specific slot. ## Extending the lists ```java @Configuration class WebConfig implements WebMvcConfigurer { @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> r) { r.add(new CurrentUserArgumentResolver()); } @Override public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> h) { h.add(new CsvReturnValueHandler()); } } ``` Critical gotcha: **custom resolvers are appended AFTER the built-ins** (they land in the `customArgumentResolvers` slot). So you cannot use a custom resolver to override a built-in for a type/annotation the built-ins already claim — the built-in matches first. To truly override, you must replace the adapter's resolver list programmatically. ## Errors - No resolver supports a parameter → `IllegalStateException` ('No suitable resolver'). - No handler supports a return type → `IllegalArgumentException` ('Unknown return value type'). - These are configuration/design errors surfaced at request time. ## When to use a custom one A custom `HandlerMethodArgumentResolver` is the idiomatic way to inject cross-cutting request-derived values (current user, tenant id, resolved from a header/JWT) as a typed method parameter instead of reading `HttpServletRequest` inside each controller. A custom return-value handler is rarer — usually you prefer a custom `HttpMessageConverter` so the standard `@ResponseBody` path handles a new media type.
- You added a custom argument resolver but it never fires for String parameters. Why?Custom resolvers are appended after the built-ins. RequestParamMethodArgumentResolver already claims simple types like String (as implicit @RequestParam), so it matches first and your resolver is never consulted. Use a distinctive annotation/type, or replace the resolver list on the adapter directly.
- How does the composite avoid re-scanning all resolvers on every request?HandlerMethodArgumentResolverComposite caches the resolved MethodParameter -> resolver mapping in a ConcurrentHashMap after the first lookup, so subsequent requests for the same parameter skip the supportsParameter scan.
saying these in an interview costs you the question
- Claiming custom argument resolvers run before the built-in ones.
- Saying @RequestBody deserialization is done directly by the adapter rather than by RequestResponseBodyMethodProcessor via HttpMessageConverters.
- Thinking argument resolvers and return-value handlers are the same list.