skip to content

How do HandlerMethodArgumentResolver and HandlerMethodReturnValueHandler work, and how does the adapter pick the right one?

level: seniorimportance: should knowfreq 40%

answer

  1. supportsParameter / resolveArgument
  2. supportsReturnType / handleReturnValue
  3. composite = first match wins, cached
  4. custom resolvers appended AFTER built-ins
  5. RequestResponseBodyMethodProcessor implements both SPIs

basics

~20 s

For 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 s

RequestMappingHandlerAdapter 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
java
// 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

for a junior

Know parameters are filled by argument resolvers and return values handled by return-value handlers, chosen by annotation/type.

for a middle

Name the two SPIs (supportsParameter/resolveArgument, supportsReturnType/handleReturnValue) and give examples per annotation.

for a senior

Explain the composites, first-match ordering + caching, and writing/registering a custom argument resolver.

for a principal

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.

context