skip to content

What is HandlerMethodReturnValueHandler, and how does Spring decide what to do with whatever a controller method returns?

level: seniorimportance: should knowfreq 40%

answer

  1. supportsReturnType -> handleReturnValue
  2. composite in RequestMappingHandlerAdapter, first match
  3. view-producing vs body-writing vs async handlers
  4. setRequestHandled(true) = body already written
  5. custom via addReturnValueHandlers, appended after defaults

basics

~20 s

It's the return-side strategy interface. After the method runs, Spring finds the first HandlerMethodReturnValueHandler whose supportsReturnType is true and calls handleReturnValue, which turns the return value into a view, a serialized body, a redirect, etc.

solid answer

~30 s

`HandlerMethodReturnValueHandler` is the SPI mirroring the argument resolver on the output side. `RequestMappingHandlerAdapter` holds a `HandlerMethodReturnValueHandlerComposite`; after invoking the handler it iterates handlers, picks the first whose `supportsReturnType(MethodParameter)` is true, and calls `handleReturnValue`, passing the same `ModelAndViewContainer`. Built-ins include `ViewNameMethodReturnValueHandler` (String view name), `ModelAndViewMethodReturnValueHandler`, `RequestResponseBodyMethodProcessor` (`@ResponseBody`, converts via `HttpMessageConverter`s), `ModelMethodProcessor` (a returned `Model`), `ViewMethodReturnValueHandler`, `HttpHeadersReturnValueHandler`, `CallableMethodReturnValueHandler`/`DeferredResult`/`StreamingResponseBody` for async, and `ModelAttributeMethodProcessor`. A handler either sets a view/model on the container or marks the request handled (writes the body directly). You extend it, like resolvers, by implementing the interface and registering via `WebMvcConfigurer.addReturnValueHandlers`, where custom handlers are appended after the defaults.

code

java · 21 lines
java
// A custom handler that serializes any @CsvBody-annotated return value as text/csv.
class CsvReturnValueHandler implements HandlerMethodReturnValueHandler {
    @Override public boolean supportsReturnType(MethodParameter returnType) {
        return returnType.hasMethodAnnotation(CsvBody.class);
    }
    @Override public void handleReturnValue(Object value, MethodParameter type,
                                            ModelAndViewContainer mav,
                                            NativeWebRequest req) throws Exception {
        HttpServletResponse res = req.getNativeResponse(HttpServletResponse.class);
        res.setContentType("text/csv");
        res.getWriter().write(Csv.render(value));
        mav.setRequestHandled(true); // response fully written; skip view resolution
    }
}

@Configuration
class WebConfig implements WebMvcConfigurer {
    @Override public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> h) {
        h.add(new CsvReturnValueHandler());
    }
}

go deeper

for a junior

Know the return side has its own strategy interface with supportsReturnType/handleReturnValue.

for a middle

Name built-in handlers and distinguish view-producing vs body-writing behavior.

for a senior

Explain setRequestHandled, dual-SPI processors, ordering, and custom registration semantics.

for a principal

Design new return abstractions via a handler; reason about ordering, async handlers, and boundaries vs advice/interceptors.

### The output-side SPI Symmetric to argument resolution, Spring converts a handler method's **return value** into an HTTP response via **`HandlerMethodReturnValueHandler`**: - `boolean supportsReturnType(MethodParameter returnType)` — can I process this return type/annotation? - `void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest)` — do the processing. ### Where it runs `RequestMappingHandlerAdapter` builds a `HandlerMethodReturnValueHandlerComposite` at startup. After the controller method executes, the composite iterates its ordered list, finds the **first** handler whose `supportsReturnType` is true, and calls `handleReturnValue`. The handler works against the shared `ModelAndViewContainer`. ### Two broad handler behaviors 1. **View-producing** handlers set a view + model on the container; the `DispatcherServlet` later resolves and renders it: - `ViewNameMethodReturnValueHandler` — a returned `String` is a logical view name (or `redirect:`/`forward:` prefix). - `ModelAndViewMethodReturnValueHandler` — a returned `ModelAndView`. - `ViewMethodReturnValueHandler` — a returned `View` instance. - `ModelMethodProcessor` / `MapMethodProcessor` — a returned `Model`/`Map` (no view name → default view name derived from the request path). 2. **Body-writing** handlers set `mavContainer.setRequestHandled(true)` and write the response directly: - `RequestResponseBodyMethodProcessor` — `@ResponseBody` / `@RestController`; serializes via `HttpMessageConverter`s using content negotiation. (Note: `ResponseEntity` shaping is covered by the Message-Conversion leaf; here we care that a return-value handler is what dispatches it.) - `HttpHeadersReturnValueHandler` — a returned `HttpHeaders`. 3. **Async** handlers start async processing and defer the real handling: - `CallableMethodReturnValueHandler`, `DeferredResultMethodReturnValueHandler`, `AsyncTaskMethodReturnValueHandler`, `StreamingResponseBodyReturnValueHandler`. ### `setRequestHandled` The container's `requestHandled` flag tells the DispatcherServlet whether a view still needs rendering. Body handlers set it `true` (nothing more to render); view handlers leave it `false` so view resolution proceeds. ### Dual-SPI processors `RequestResponseBodyMethodProcessor` and `ModelAttributeMethodProcessor` implement **both** `HandlerMethodArgumentResolver` and `HandlerMethodReturnValueHandler` (e.g. `@RequestBody` in, `@ResponseBody` out), so the same instance appears in both composites. ### Extending it ```java @Configuration class WebConfig implements WebMvcConfigurer { @Override public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> h) { h.add(new CsvReturnValueHandler()); } } ``` As with resolvers, custom handlers are **appended after the defaults**, so you can handle novel return types but cannot preempt built-in handling of, say, `String` or `@ResponseBody`. To reorder/replace you'd set the full list on `RequestMappingHandlerAdapter`. ### Ordering subtlety Order matters because of first-match. For example, `ModelAndViewMethodReturnValueHandler` and `ViewNameMethodReturnValueHandler` are placed before the generic model handlers; async handlers are placed early so a `Callable`/`DeferredResult` is recognized before anything else. ### Gotchas - A method annotated `@ResponseBody` returning a `String` is treated as a **response body**, NOT a view name — because the `@ResponseBody` processor's `supportsReturnType` matches first. - `void` return with a `Model` param (or a `ServletResponse` param) means the handler is assumed to have written the response or relies on a default/RequestToViewNameTranslator-derived view. - Custom handlers must be **stateless** (singletons) and pure in `supportsReturnType` (result cached). - Don't confuse this with `ResponseBodyAdvice`/`HandlerInterceptor` — those wrap/augment, they don't select the base handling strategy. ### When to use Add a custom return-value handler to support a new return abstraction uniformly across controllers (e.g. a `Result<T>`, a CSV/streaming type, a domain-specific view descriptor) without repeating serialization logic in every method.

  • A method has @ResponseBody and returns the String "orders". Is that a view name?
    No. Because @ResponseBody is present, RequestResponseBodyMethodProcessor's supportsReturnType matches first and writes "orders" as the response body via message converters. Without @ResponseBody, ViewNameMethodReturnValueHandler would treat it as a logical view name.
  • How does the DispatcherServlet know a return-value handler already wrote the response?
    The handler calls mavContainer.setRequestHandled(true). The adapter then returns a null ModelAndView, signaling the DispatcherServlet that no view resolution/rendering is needed.

saying these in an interview costs you the question

  • Believing every returned String is always a view name (ignores @ResponseBody)
  • Thinking multiple return-value handlers process the same return value in sequence
  • Confusing HandlerMethodReturnValueHandler with ResponseBodyAdvice or HandlerInterceptor
  • Assuming custom handlers can override built-in @ResponseBody handling via addReturnValueHandlers

context