skip to content

Walk through how the DispatcherServlet decides whether a controller's return value is a view or a serialized body.

level: seniorimportance: should knowfreq 35%

answer

  1. HandlerMethodReturnValueHandler chain, first match wins
  2. RequestResponseBodyMethodProcessor -> converters -> setRequestHandled(true)
  3. no body-handler -> ViewNameMethodReturnValueHandler -> ViewResolver
  4. content negotiation picks the converter
  5. @ResponseBody checked on method OR class

basics

~20 s

After the handler runs, RequestMappingHandlerAdapter passes the return value to a chain of return-value handlers. If @ResponseBody is present (method or class), RequestResponseBodyMethodProcessor serializes it via HttpMessageConverters. Otherwise it's treated as a view name and resolved by a ViewResolver.

solid answer

~40 s

The DispatcherServlet delegates invocation to RequestMappingHandlerAdapter, which after calling the handler passes the return value through an ordered list of HandlerMethodReturnValueHandlers. Each handler's supportsReturnType() is checked in order. RequestResponseBodyMethodProcessor claims the value when @ResponseBody is present on the method or declaring class (merged-annotation aware) — it then runs content negotiation and writes the body with a matching HttpMessageConverter, marking the request handled so no view resolution happens. ResponseEntity is handled by HttpEntityMethodProcessor similarly. If no body-handler claims it, a String/View/ModelAndView is handled by ViewNameMethodReturnValueHandler / ModelAndViewMethodReturnValueHandler, which sets a view name; DispatcherServlet then runs ViewResolvers to get a View and render it. So the @Controller-vs-@RestController distinction ultimately reduces to which return-value handler wins, driven by the presence of @ResponseBody.

code

java · 18 lines
java
// Same return type, opposite dispatch, decided by @ResponseBody presence:

@Controller
class ViewCtrl {
    @GetMapping("/a")
    String a() { return "home"; }        // ViewNameMethodReturnValueHandler -> render templates/home
}

@RestController
class BodyCtrl {
    @GetMapping("/b")
    String b() { return "home"; }        // RequestResponseBodyMethodProcessor -> body "home" (text/plain)

    @GetMapping("/c")
    ResponseEntity<String> c() {          // HttpEntityMethodProcessor -> full control
        return ResponseEntity.status(201).body("created");
    }
}

go deeper

for a junior

Not expected — this is internals.

for a middle

Should know @ResponseBody triggers serialization and otherwise a String is a view name.

for a senior

Should name the return-value-handler chain and RequestResponseBodyMethodProcessor / ViewNameMethodReturnValueHandler and content negotiation.

for a principal

Discusses handler ordering, ResponseBodyAdvice, 406 negotiation, and requestHandled short-circuit precisely.

## The dispatch pipeline 1. **DispatcherServlet** receives the request, uses **HandlerMapping** (`RequestMappingHandlerMapping`) to find the `HandlerMethod`. 2. It picks a **HandlerAdapter** (`RequestMappingHandlerAdapter`) and calls `handle()`. 3. The adapter resolves arguments, **invokes** the controller method, and gets a **return value** + its `MethodParameter` (which carries annotation info). 4. The adapter asks its ordered **`HandlerMethodReturnValueHandlerComposite`** to handle the value: it iterates handlers, calling `supportsReturnType(returnType)` until one matches, then `handleReturnValue(...)`. ## The decision point The key handlers, roughly in priority: - **`RequestResponseBodyMethodProcessor`** — `supportsReturnType` is true when `@ResponseBody` is present on the **method or the containing class** (looked up via `AnnotatedElementUtils`, so class-level `@ResponseBody` from `@RestController` counts). It performs **content negotiation** (Accept header vs producible media types), selects an `HttpMessageConverter` (e.g. `MappingJackson2HttpMessageConverter`), writes the serialized body, and calls `mavContainer.setRequestHandled(true)` — signaling **no view rendering**. - **`HttpEntityMethodProcessor`** — handles `ResponseEntity`/`HttpEntity` returns (status + headers + converter-written body), regardless of the stereotype. - **`ViewNameMethodReturnValueHandler`** — handles a `String` return (when *not* `@ResponseBody`) by setting it as the view name on the `ModelAndViewContainer`. - **`ModelAndViewMethodReturnValueHandler`** — handles `ModelAndView` returns. - **`ViewMethodReturnValueHandler`** — handles a returned `View` object. ## After the handler runs If `requestHandled` is true (body was written), the DispatcherServlet **skips** view resolution — the response is complete. Otherwise it builds a `ModelAndView`, runs the registered **`ViewResolver`s** (e.g. `ThymeleafViewResolver`) to turn the view name into a `View`, and calls `view.render(model, request, response)` to produce HTML. ## Why the same String means two different things - Plain `@Controller` method returning `"home"` → no body-handler matches → `ViewNameMethodReturnValueHandler` → view name → template `home`. - `@RestController` (class-level `@ResponseBody`) method returning `"home"` → `RequestResponseBodyMethodProcessor` matches first → `StringHttpMessageConverter` writes the literal text `home` as `text/plain`. That is the entire practical difference, expressed at the return-value-handler layer. ## Edge cases & gotchas - **`void` / no return + `@ResponseBody`**: the method is expected to write to the response itself (e.g. via `HttpServletResponse` argument), else an empty body. - **Content negotiation failure**: if no converter can produce a type the client's `Accept` allows, you get `406 Not Acceptable` (`HttpMediaTypeNotAcceptableException`). - **`@ResponseStatus`** and `ResponseEntity` interact: `ResponseEntity`'s status wins for that return; `@ResponseStatus` on the method/exception sets status when not otherwise specified. - **`ResponseBodyAdvice`** (e.g. for `@JsonView`, HATEOAS, or global wrapping) can post-process the body after the converter is chosen but before writing. - The ordering of return-value handlers matters: body/entity processors are consulted before the view-name handler, which is why `@ResponseBody` reliably wins.

  • What HTTP status results if a @RestController must return JSON but the client sends Accept: text/csv and no CSV converter exists?
    406 Not Acceptable — content negotiation finds no HttpMessageConverter that can produce a media type the client accepts, and Spring raises HttpMediaTypeNotAcceptableException.
  • How does ResponseBodyAdvice fit into this flow?
    After RequestResponseBodyMethodProcessor selects a converter but before writing, registered ResponseBodyAdvice beans (e.g. for @JsonView filtering or global envelope wrapping) can inspect/modify the body. It's the hook for cross-cutting body transformations.

saying these in an interview costs you the question

  • Saying view resolution always runs and @ResponseBody just 'skips the template' (body-writing sets requestHandled=true, ViewResolvers never run)
  • Thinking Jackson is invoked directly by the controller rather than by a converter chosen via content negotiation
  • Believing a String is always JSON-serialized (StringHttpMessageConverter writes raw text/plain)

context