Walk through the doDispatch() flow: what happens inside DispatcherServlet when a request arrives?
answer
- multipart → getHandler → getHandlerAdapter
- preHandle (false = stop) → handle → postHandle
- processDispatchResult = exceptions + render
- afterCompletion always fires (reverse order)
- @ResponseBody ⇒ ModelAndView null; async ⇒ re-dispatch
basics
~10 sdoDispatch finds a handler via HandlerMapping, gets a HandlerAdapter, runs interceptor preHandle, invokes the handler to get a ModelAndView, runs postHandle, then renders the view (or writes the body) and runs afterCompletion.
solid answer
~40 sInside `doDispatch`, DispatcherServlet: (1) checks for multipart and wraps the request; (2) calls `getHandler` — iterates HandlerMappings to get a `HandlerExecutionChain` (handler + interceptors); if none, sends 404 or throws NoHandlerFoundException; (3) calls `getHandlerAdapter` for a matching HandlerAdapter; (4) runs interceptor `preHandle` (if any returns false, it stops); (5) calls `ha.handle(...)` which invokes the controller and returns a `ModelAndView` (null for @ResponseBody, since the body was already written); (6) applies the default view name if needed; (7) runs interceptor `postHandle`; (8) `processDispatchResult` — resolves exceptions via HandlerExceptionResolver, renders the view, then runs `afterCompletion`. The whole body is wrapped so `afterCompletion` always fires and multipart resources are cleaned up.
code
java · 16 lines// Simplified shape of DispatcherServlet.doDispatch (conceptual)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
HttpServletRequest processedRequest = checkMultipart(request);
HandlerExecutionChain mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; }
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
if (!mappedHandler.applyPreHandle(processedRequest, response)) return;
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
processDispatchResult(processedRequest, response, mappedHandler, mv, /*exception*/ null);
// render() + triggerAfterCompletion() happen inside processDispatchResult / finally
}go deeper
Roughly: find handler, call it, render/return response.
Name the ordered steps and the preHandle/postHandle/afterCompletion interceptor points.
Explain HandlerAdapter indirection, exception resolution path, and the @ResponseBody null-ModelAndView case.
Discuss async re-dispatch, guaranteed cleanup/afterCompletion semantics, and interceptor ordering guarantees.
## The doDispatch pipeline `doDispatch(HttpServletRequest, HttpServletResponse)` is the protected method in `DispatcherServlet` that runs for every request (called from `doService`, which sets up framework request attributes first). Step by step: ### 1. Multipart resolution If a `MultipartResolver` is present and the request is multipart, the request is wrapped in a `MultipartHttpServletRequest`. A boolean `multipartRequestParsed` is tracked so resources get cleaned up at the end. ### 2. getHandler — find the handler ```java HandlerExecutionChain mappedHandler = getHandler(processedRequest); ``` DispatcherServlet iterates its ordered list of `HandlerMapping` beans and returns the **first** non-null `HandlerExecutionChain`. That chain bundles the **handler** (usually a `HandlerMethod`) plus the applicable **HandlerInterceptors**. If no mapping matches: if `throwExceptionIfNoHandlerFound` is true it throws `NoHandlerFoundException`, otherwise it sends a 404. (In recent Spring Boot this throw-behavior is effectively on.) ### 3. getHandlerAdapter ```java HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); ``` It finds the first HandlerAdapter whose `supports(handler)` returns true (e.g. `RequestMappingHandlerAdapter` for `@RequestMapping` methods). This decouples DispatcherServlet from *how* a handler is invoked. ### 4. Last-Modified / GET short-circuit For GET/HEAD it may honor `Last-Modified` and return 304 without invoking the handler. ### 5. Interceptor preHandle ```java if (!mappedHandler.applyPreHandle(processedRequest, response)) return; ``` Interceptors run in order; if any returns `false`, processing stops and (importantly) already-run interceptors get `afterCompletion` — the handler is never invoked. ### 6. Invoke the handler ```java mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ``` The HandlerAdapter resolves method arguments (`@RequestParam`, `@RequestBody`, etc.), calls the controller method, and processes the return value. For `@ResponseBody`/`ResponseEntity` the body is written via `HttpMessageConverter`s during this step and `mv` is `null`. For a view-based controller, `mv` is a `ModelAndView`. ### 7. Default view name `applyDefaultViewName` uses a `RequestToViewNameTranslator` if the controller returned a model but no view name. ### 8. postHandle `mappedHandler.applyPostHandle(...)` runs interceptor `postHandle` (before rendering, so interceptors can still tweak the ModelAndView). ### 9. processDispatchResult This handles the outcome: - If an **exception** was thrown during handling, it's caught and passed to `processHandlerException`, which walks the ordered `HandlerExceptionResolver`s (including the one backing `@ExceptionHandler`/`@ControllerAdvice`) to produce an error ModelAndView or write an error response. - Otherwise, if there's a ModelAndView, it calls **`render(mv, ...)`** → resolves the view via `ViewResolver`s and calls `view.render(model, request, response)`. ### 10. afterCompletion & cleanup `triggerAfterCompletion` runs interceptor `afterCompletion` (in reverse order) — always, even on exceptions. Finally multipart resources are cleaned. All wrapped in try/catch/finally so cleanup is guaranteed. ## Gotchas & edge cases - **Async**: if the handler starts async processing (`Callable`, `DeferredResult`, `WebAsyncTask`), `doDispatch` returns early and the request is later **re-dispatched** — `postHandle`/`render` happen on the async dispatch, not the original. - **`@ResponseBody` returns null ModelAndView** — don't expect a view; the body is already committed. - **Interceptor ordering**: `afterCompletion` fires in reverse, and only for interceptors whose `preHandle` succeeded. - **NoHandlerFoundException** is only thrown if configured; otherwise you silently get a container 404 that your `@ControllerAdvice` may not see.
- If an interceptor's preHandle returns false, is the controller invoked and does afterCompletion run?The controller is NOT invoked. afterCompletion runs only for interceptors whose preHandle already returned true; the one that returned false and later ones do not get afterCompletion.
- For an @ResponseBody controller, what does ha.handle return and where is the body written?It returns null (no ModelAndView). The response body was already written by HttpMessageConverters inside the HandlerAdapter, so there's nothing to render.
- Where do exceptions thrown by the controller get handled?In processDispatchResult → processHandlerException, which delegates to the ordered HandlerExceptionResolvers (including @ExceptionHandler/@ControllerAdvice support).
saying these in an interview costs you the question
- Saying DispatcherServlet calls the controller method directly (it goes through a HandlerAdapter)
- Thinking render always runs for REST controllers
- Believing afterCompletion runs for all interceptors regardless of preHandle result
- Claiming postHandle runs after rendering