The Front Controller Pattern: How DispatcherServlet Routes HTTP Requests
Understanding Servlet API primitives, DispatcherServlet initialization, and the doDispatch execution loop
Part 11 in Series — Catch up on the previous article: Spring Boot Starters & Embedded Web Servers: How Tomcat Runs Inside an Executable JAR (Part 10) before diving into this post.
Why You Need This in Real Life
Building a multi-tenant SaaS application in raw Java Servlets without a framework forces every endpoint to extend HttpServlet: OrderServlet, UserServlet, PaymentServlet. Each servlet must manually duplicate security checks, character encoding filters, JSON parsing, database connection opening/closing, and exception formatting:
public class OrderServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// Duplicate Security Check
if (!Authenticator.isAuthenticated(req)) {
resp.sendError(401, "Unauthorized");
return;
}
// Duplicate UTF-8 Encoding
req.setCharacterEncoding("UTF-8");
// Duplicate JSON Parsing & Exception Handling...
}
}
When compliance requirements force you to append an audit tracking header to every API response, you must edit 40 individual Servlets.
The Front Controller Pattern solves this maintenance nightmare by providing a single centralized HTTP entry point that handles cross-cutting concerns (authentication, locale resolution, logging, error handling) before delegating to specialized handler methods. In Spring MVC, that central engine is DispatcherServlet.
Part 1: Evolution of the Servlet API & Front Controller Pattern
Traditional Servlet Architecture vs Front Controller Architecture
In standard Java EE applications, the servlet container (Tomcat) inspects URL patterns and dispatches incoming sockets directly to matching Servlets registered in web.xml:
Traditional Servlet Mapping:
GET /orders -----> OrderServlet.doGet()
POST /users -----> UserServlet.doPost()
GET /items -----> ItemServlet.doGet()
Front Controller Pattern (DispatcherServlet):
GET /orders ---\
POST /users ----> [ DispatcherServlet ] ---> Centralized Interceptors/Filters ---> Handler Method
GET /items ---/ (Front Controller)
By mapping DispatcherServlet to /* or /, Spring MVC captures every incoming HTTP request, transforming raw HTTP stream data into high-level Java method invocations.
Part 2: Dissecting DispatcherServlet Inheritance Hierarchy
DispatcherServlet is not a standalone framework magic class; it is a standard jakarta.servlet.http.HttpServlet descendant:
jakarta.servlet.Servlet (Interface)
|
jakarta.servlet.GenericServlet
|
jakarta.servlet.http.HttpServlet
|
org.springframework.web.servlet.HttpServletBean
|
org.springframework.web.servlet.FrameworkServlet
|
org.springframework.web.servlet.DispatcherServlet
Hierarchy Responsibilities
HttpServletBean: Wraps servlet initialization parameters fromServletConfiginto SpringBeanWrapperproperties.FrameworkServlet: Integrates the Servlet with a SpringApplicationContext. Overrides standarddoGet(),doPost(),doPut(),doDelete()methods fromHttpServlet, redirecting them all to a single central method:processRequest(HttpServletRequest request, HttpServletResponse response).DispatcherServlet: ReceivesprocessRequest()calls and delegates execution todoDispatch(request, response).
Part 3: DispatcherServlet Strategy Initialization
When Tomcat initializes DispatcherServlet, Spring executes onRefresh(ApplicationContext context) to resolve and cache nine strategic infrastructure components.
// Inside DispatcherServlet.java
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context); // Maps URIs to HandlerMethods
initHandlerAdapters(context); // Executes HandlerMethods with argument resolution
initHandlerExceptionResolvers(context); // Handles exceptions
initRequestToViewNameTranslator(context);
initViewResolvers(context); // Resolves HTML/JSP views
initFlashMapManager(context);
}
If custom beans for these strategies are not present in your ApplicationContext, DispatcherServlet loads default implementations specified in DispatcherServlet.properties on the classpath (e.g., RequestMappingHandlerMapping, RequestMappingHandlerAdapter).
Part 4: The Heart of Spring MVC: doDispatch() Execution Loop
Every HTTP request routed through Spring MVC is processed by doDispatch(). Below is a trace of the internal execution loop:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 1. Check for Multipart (File upload) request
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 2. Determine handler (Controller method) for current request
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 3. Determine handler adapter for handler
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 4. Apply preHandle() on registered HandlerInterceptors
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return; // An interceptor rejected the request (e.g., Auth failure)
}
// 5. Actually invoke the handler (Controller method execution)
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 6. Apply postHandle() on registered HandlerInterceptors
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new ServletException("Handler dispatch failed", err);
}
// 7. Process result (Render view or handle exception)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
+-----------------------------------------------------------------------------+
| doDispatch Execution Pipeline |
| |
| Incoming HttpServletRequest |
| | |
| v |
| 1. getHandler(request) ---------> Finds HandlerExecutionChain |
| | (HandlerMethod + HandlerInterceptors) |
| v |
| 2. applyPreHandle() ------------> Executes interceptor.preHandle() |
| | (Returns false -> abort request) |
| v |
| 3. HandlerAdapter.handle() -----> Resolves args & executes @Controller |
| | |
| v |
| 4. applyPostHandle() -----------> Executes interceptor.postHandle() |
| | |
| v |
| 5. processDispatchResult() -----> Resolves View or @ExceptionHandler |
| | |
| v |
| 6. triggerAfterCompletion() ----> Executes interceptor.afterCompletion() |
+-----------------------------------------------------------------------------+
Part 5: Edge Cases & Real-World Gotchas
Gotcha 1: Blocking Interceptor preHandle() Cleanups
When HandlerInterceptor.preHandle() returns false, doDispatch() aborts execution immediately without calling postHandle(). However, interceptors that executed successfully prior to the failing interceptor must still have their afterCompletion() method called to prevent ThreadLocal memory leaks (e.g., clearing security context or tenant ID).
// Inside HandlerExecutionChain.java
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
// Trigger afterCompletion ONLY for interceptors that already passed preHandle
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i; // Track last successful interceptor index
}
return true;
}
Gotcha 2: Async Request Processing Thread Hand-Off
When a controller returns a CompletableFuture or DeferredResult, Tomcat’s worker thread is released back to the server thread pool immediately while asynchronous processing occurs on a background thread. When the async computation completes, the container performs a Servlet Dispatch Re-entry (AsyncContext.dispatch()), causing doDispatch() to execute a second time to render the HTTP response.
Next Steps
Now that we understand DispatcherServlet and the doDispatch() execution loop, we will examine how Spring MVC maps URIs to controller methods via HandlerMapping, resolves method arguments using HandlerMethodArgumentResolver, and serializes HTTP responses with HttpMessageConverter.
References & Further Reading
- Spring.io. Spring Framework Web Servlet Technology (DispatcherServlet, HandlerMapping). Spring Docs.
- Eclipse Foundation. Jakarta Servlet Specification 6.0. Jakarta EE Docs.
- Walls, C. (2022). Spring in Action (6th Edition) — Chapter 2: Developing Web Applications. Manning.
Part 12: Spring MVC Request Lifecycle: HandlerMappings, HandlerAdapters, and MessageConverters
Continue to Part 12 →