What this post covers
This is the twenty-first post in the "Digging into Spring's Source" series. Part 20 looked at the floor of the web side, the spring-web module. This post is unit U19 on the map from Part 1, the second unit of Phase 5 (web): DispatcherServlet, the class at the very center of Spring MVC. Every request a browser sends passes through this one class, and this class decides who will handle it and hands the work out.
We want to answer four questions.
- What does
DispatcherServletinherit, and what is each inherited layer responsible for? - When the servlet starts, when is the Spring container created, and where do the parts used for request handling come from?
- When a request arrives, what is called inside
doDispatch(), and in what order? - How does that order change when there is no handler, or when an exception is thrown along the way?
| Item | Value |
|---|---|
| Source | Spring Framework 7.1.0-SNAPSHOT |
| Commit | f447f3c310 (2026-10-01, main branch) |
| Main module | spring-webmvc (package org.springframework.web.servlet) |
| Main classes | HttpServletBean, FrameworkServlet, DispatcherServlet, HandlerExecutionChain |
All line numbers and details in this post refer to this commit. In other versions the line numbers differ and the details may too. Part 1 explains how to check out the same commit.
This post only follows the main thread. DispatcherServlet does not do the work itself; it tells a number of parts to do it, and going inside each part would never end. Here we only look at who is called when. The inside of the parts that find and call handlers is left to Part 22, and the inside of exception handling, interceptors and views to Part 23.
Terms to know first
A servlet is the standard Java way of handling web requests. A servlet container such as Tomcat receives the HTTP request, creates an HttpServletRequest (the request) and an HttpServletResponse (the response), and calls the service() method of a registered servlet. It calls init() once before first use and destroy() once when taking the servlet down. The classes of this standard (the jakarta.servlet package) are not Spring's, so their source is not in this repository.
A front controller is a structure where one place receives every request first and passes it on to the right handler. Instead of one servlet per URL, a single servlet takes them all. DispatcherServlet is that single servlet. To dispatch means to send off, to assign.
A handler is the object that actually processes a request. A method of a controller you write is the typical example. In the source of DispatcherServlet, the type of a handler is simply Object. That lets an object of any shape be a handler, and it is why the next two parts are needed.
A HandlerMapping looks at a request and says "this handler takes this request". A HandlerAdapter knows how to actually call that handler. Because handlers come in different shapes, the knowledge of how to call them is kept in a separate part. The inside of both is the topic of Part 22.
An interceptor (HandlerInterceptor) is an object that steps in before and after the handler. It has three methods: preHandle, called before the handler; postHandle, called after the handler; and afterCompletion, called after everything is done (HandlerInterceptor line 102, line 129, line 154).
A strategy object is a part that has been split off so that it can be swapped: "this job is delegated to this object". HandlerMapping and HandlerAdapter above are strategy objects, and the comments in DispatcherServlet call them strategies too (line 438).
A WebApplicationContext is the Spring container for web environments. The container and refresh() are covered in Part 7, and what the web context adds is in Part 20.
1. Four layers of inheritance — each layer has its own job
Follow the declaration of DispatcherServlet upward and you find four layers.
DispatcherServlet extends FrameworkServlet(line 157)FrameworkServlet extends HttpServletBean(line 142)HttpServletBean extends HttpServlet(line 82)HttpServlet— a class of the Servlet standard
Inheritance runs from top to bottom. Gray is a class outside Spring (the Servlet standard); blue is the subject of this post.
Each layer fills in a method the layer above left empty, and leaves an empty method of its own for the layer below.
| Layer | At startup | When a request arrives | Slot left for the layer below |
|---|---|---|---|
HttpServlet |
where the init() call starts |
service() looks at the HTTP method and calls doGet(), doPost(), ... |
init(), doGet(), ... |
HttpServletBean |
init(): copies the servlet's settings (init-params) onto its own properties |
— | initServletBean() |
FrameworkServlet |
initServletBean(): sets up its own WebApplicationContext |
funnels doGet() and the rest into processRequest() |
onRefresh(), doService() |
DispatcherServlet |
onRefresh(): collects the strategy objects |
doService() → doDispatch(): finds a handler and calls it |
— |
The behavior of HttpServlet at the top belongs to the Servlet standard, so it is not something this post verified in source. But Spring's source is written on that assumption: the Javadoc of FrameworkServlet.doGet() notes that "HttpServlet's default implementation of doHead" will also invoke it (lines 880–885).
A structure where the upper layer holds the flow and the lower layer only fills in slots is the template method pattern we saw with refresh() in Part 7. FrameworkServlet marks initServletBean() and processRequest() as final (line 518, line 983), so the layer below cannot change the flow itself.
Easy to get wrong
DispatcherServletis a servlet before it is a Spring bean. The one that creates it and callsinit()is the servlet container, not the Spring container. So the order is the reverse of what you might expect: the container does not create the servlet; the servlet, as it starts, sets up the Spring container it will use (section 2).doGet()anddoPost()do the same thing. When you write a servlet by hand you put different code in the two, butFrameworkServletfillsdoGet,doPost,doPut,doDeleteanddoPatchwith the same single line,processRequest(request, response)(lines 886–936). Telling HTTP methods apart happens much later, in the part that picks the handler.
2. At startup: the servlet sets up its own context
When the servlet container calls init(), the call travels down through three layers.
From init() to initStrategies(). The blue boxes are the route by which the news "refresh is done" comes back to the servlet.
HttpServletBean.init() — settings become properties
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
// ...
// Let subclasses do whatever initialization they like.
initServletBean();
}
spring-webmvc/src/main/java/org/springframework/web/servlet/HttpServletBean.java lines 149–172
The part cut out takes the servlet settings (init-params) written in web.xml or elsewhere and applies them through setXxx() methods. If there is a setting named contextConfigLocation, setContextConfigLocation() gets called, and so on. BeanWrapper, the tool that puts strings into properties, was covered in Part 14. After that it calls initServletBean(), the slot for the layer below. In HttpServletBean that method is empty (lines 192–193).
FrameworkServlet.initWebApplicationContext() — three ways to get a context
The initServletBean() that FrameworkServlet fills in does nothing but log, call initWebApplicationContext(), and then call initFrameworkServlet(), an empty slot for subclasses (lines 807–808) (lines 518–545). The real work is here.
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext cwac && !cwac.isActive()) {
// ...
configureAndRefreshWebApplicationContext(cwac);
}
}
if (wac == null) {
// ...
wac = findWebApplicationContext();
}
if (wac == null) {
// ...
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
// ...
return wac;
}
spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java lines 556–603
Reading from the top:
- Look up the root context.
rootContextis a context shared by the whole application that may already be running before the servlet. If there is none it isnull. Who starts it is covered in Part 20. - If a context was given to the constructor, use it. That is the
new DispatcherServlet(context)case (DispatcherServletlines 366–369). If it has not been refreshed yet, the root context becomes its parent when it has none, andconfigureAndRefreshWebApplicationContext()goes on to refresh it. - Otherwise, look in the servlet context attributes. This only applies when
contextAttributehas been set (lines 615–626). - If there still is none, create one.
createWebApplicationContext(rootContext)instantiates the context class, sets the root context as parent, and calls the sameconfigureAndRefreshWebApplicationContext()as in step 2 (lines 643–663). The context class used when nothing else is configured isXmlWebApplicationContext(line 155). - Call
onRefresh(wac)directly — only if it has not been called yet. We will see right below why this line exists.
Whichever way it goes, the servlet ends up with a context of its own, and if there is a root context, that becomes the parent. A web application can have several DispatcherServlets; each has its own context and only the root context is shared, as the class Javadoc says (lines 136–139).
The news that refresh is done comes back as an event
Two lines in configureAndRefreshWebApplicationContext() matter.
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
// ...
wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
// ...
postProcessWebApplicationContext(wac);
applyInitializers(wac);
wac.refresh();
}
spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java lines 665–695
The last line, wac.refresh(), runs the twelve steps from Part 7. This is when all the beans, controllers included, are created. And before it, addApplicationListener(...) attaches a listener to this context. The last step of refresh() publishes a ContextRefreshedEvent (AbstractApplicationContext line 1017; how events are delivered is in Part 12), and the listener just attached, ContextRefreshListener, receives it and passes it to the servlet's onApplicationEvent() (lines 1179–1185).
public void onApplicationEvent(ContextRefreshedEvent event) {
this.refreshEventReceived = true;
synchronized (this.onRefreshMonitor) {
onRefresh(event.getApplicationContext());
}
}
spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java lines 830–835
It sets refreshEventReceived to true and calls onRefresh(). Step 5 above, the if (!this.refreshEventReceived) in initWebApplicationContext(), looks at this flag. If the event arrived and onRefresh() has already run, it is skipped; if no event arrived (for example because the context handed in was already refreshed), it is called directly.
Why go through an event instead of calling onRefresh() on the line right after refresh()? Because the context may be refreshed again later. FrameworkServlet even has a refresh() method that refreshes its own context (lines 816–822). When a context is refreshed again its beans are recreated, so the parts the servlet was holding must be collected again. Hooking onto the event means onRefresh() follows whenever and by whomever the context is refreshed.
The SourceFilteringListener wrapped around the listener passes the event on only if its source is this context (SourceFilteringListener lines 71–75). As Part 12 showed, events are also forwarded to the parent context, so without filtering by source the servlet would react to the refresh of other contexts too.
Easy to get wrong: there are two onRefresh methods
- Step 9 in Part 7,
AbstractApplicationContext.onRefresh(), is a slot of the context, called before the singletons are created. FrameworkServlet.onRefresh(ApplicationContext)in this post is a slot of the servlet, called after refresh has completely finished (lines 844–846).
Only the name is the same; the owner and the moment are different. The latter must be called after all beans exist, because its job is to look up beans in the context and collect them (section 3).
Example 1: the two routes to onRefresh
Let's see both branches. We make a DispatcherServlet subclass that prints whether onApplicationEvent is on the call stack when onRefresh() is called, then hand it a context that has not been refreshed, and one that already has. To run without a servlet container, we use the fake objects MockServletConfig and MockServletContext from spring-test (spring-test itself is read in Part 26). Below is ex/u19/ServletInitExample.java minus the imports and the outer class declaration.
// An example written for this post (not Spring's code)
/** A DispatcherServlet that prints who called onRefresh. */
static class TracingServlet extends DispatcherServlet {
TracingServlet(AnnotationConfigWebApplicationContext context) {
super(context);
}
@Override
protected void onRefresh(ApplicationContext context) {
boolean viaEvent = StackWalker.getInstance().walk(frames ->
frames.anyMatch(frame -> frame.getMethodName().equals("onApplicationEvent")));
System.out.println(" onRefresh called "
+ (viaEvent ? "via ContextRefreshedEvent" : "directly by initWebApplicationContext"));
super.onRefresh(context);
}
}
public static void main(String[] args) throws Exception {
MockServletContext servletContext = new MockServletContext();
System.out.println("[1] context not refreshed yet");
AnnotationConfigWebApplicationContext fresh = new AnnotationConfigWebApplicationContext();
TracingServlet first = new TracingServlet(fresh);
System.out.println(" before init: isActive=" + fresh.isActive()
+ ", getHandlerMappings()=" + first.getHandlerMappings());
first.init(new MockServletConfig(servletContext, "first"));
System.out.println(" after init: isActive=" + fresh.isActive()
+ ", handlerMappings=" + first.getHandlerMappings().size());
System.out.println(" context id=" + fresh.getId());
System.out.println(" ServletContext attribute=" + first.getServletContextAttributeName());
System.out.println("[2] context already refreshed");
AnnotationConfigWebApplicationContext refreshed = new AnnotationConfigWebApplicationContext();
refreshed.setServletContext(servletContext);
refreshed.refresh();
TracingServlet second = new TracingServlet(refreshed);
second.init(new MockServletConfig(servletContext, "second"));
System.out.println(" after init: handlerMappings=" + second.getHandlerMappings().size());
first.destroy();
second.destroy();
System.out.println("[3] after destroy: first context isActive=" + fresh.isActive()
+ ", second context isActive=" + refreshed.isActive());
refreshed.close();
}
The output:
[1] context not refreshed yet
before init: isActive=false, getHandlerMappings()=null
onRefresh called via ContextRefreshedEvent
after init: isActive=true, handlerMappings=3
context id=org.springframework.web.context.WebApplicationContext:/first
ServletContext attribute=org.springframework.web.servlet.FrameworkServlet.CONTEXT.first
[2] context already refreshed
onRefresh called directly by initWebApplicationContext
after init: handlerMappings=3
[3] after destroy: first context isActive=false, second context isActive=false
This example was run on the released Spring Framework 7.0.9 and produced the output above (the 7.1.0-SNAPSHOT this series reads was not built and run directly). The output confirms the following.
- In [1],
init()refreshed the context, andonRefresh()was called via the event. Beforeinit()the context is inactive andgetHandlerMappings()isnull. - The servlet name
firstappears in the context id and in the servlet context attribute name. The id is assigned byconfigureAndRefreshWebApplicationContext()(lines 666–677), and the attribute is set at the end ofinitWebApplicationContext()(thepublishContextpart cut out above, lines 596–600) underFrameworkServlet.CONTEXT.plus the servlet name (lines 789–791). - In [2], no event arrived, so
initWebApplicationContext()called it directly. The context had finished refreshing before it ever met the servlet, so there was no chance to attach a listener. destroy()closed both contexts.destroy()leaves the context open only when it was injected throughsetApplicationContext()(lines 852–860). For a context passed to the constructor,webApplicationContextInjectedstaysfalse(lines 289–291, lines 505–510), so it is closed.
3. Collecting the strategy objects — initStrategies()
The onRefresh() that DispatcherServlet fills in is one line, initStrategies(context) (lines 433–435).
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java lines 441–450
Eight lines set up eight kinds of parts, one each, and store them in fields of the servlet (lines 279–301).
| Part | What it does | How many | Found by | Covered in |
|---|---|---|---|---|
MultipartResolver |
unpacks file-upload requests | one | name multipartResolver |
— |
LocaleResolver |
decides the language and region of this request | one | name localeResolver |
— |
HandlerMapping |
request → handler | several | type | Part 22 |
HandlerAdapter |
calls the handler | several | type | Part 22 |
HandlerExceptionResolver |
exception → response | several | type | Part 23 |
RequestToViewNameTranslator |
makes up a view name from the request when there is none | one | name viewNameTranslator |
— |
ViewResolver |
view name → view object | several | type | Part 23 |
FlashMapManager |
carries values briefly across a redirect | one | name flashMapManager |
— |
The four found "by name" use context.getBean("name", type); the four found "by type" collect every bean of that type. The names are constants at the top of the class (lines 160–201). The four that collect several all look alike, so we only read the one for HandlerMapping.
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
// ...
// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
// ...
}
// ...
}
spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java lines 506–545
beansOfTypeIncludingAncestors(...)finds every bean of typeHandlerMappingin this context and its parent contexts. Ones placed in the root context are found too.- If any were found, they go into a list and are sorted by order, the order given by
@OrderorOrdered. That order becomes "who gets asked first" in section 5. - If none were found,
getDefaultStrategies()supplies the defaults.
The defaults when there are no beans — DispatcherServlet.properties
The defaults are written in a file, not in code: DispatcherServlet.properties, in the same package as the DispatcherServlet class.
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
org.springframework.web.servlet.function.support.RouterFunctionMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
org.springframework.web.servlet.function.support.HandlerFunctionAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
On the left is the name of a strategy interface, on the right the names of implementation classes. getDefaultStrategies() reads this file once into a static field (lines 767–778), looks up the value by interface name, splits it on commas, and calls createDefaultStrategy() for each class to create an object (lines 780–803). createDefaultStrategy() asks the context's bean factory to createBean(clazz) (lines 819–821), so that the object goes through the instantiation, injection and initialization we saw in Part 4. But an object created this way is not registered as a bean in the context (Example 2 confirms it).
Yellow boxes are where values are kept. Only MultipartResolver has no default.
Example 2: the default strategies, and what happens when you register one bean
We start the servlet once with a context that has no beans and once with a context that has a single HandlerMapping bean, and print getHandlerMappings() (lines 734–736). The full code is ex/u19/DefaultStrategiesExample.java; below are the two configuration classes and the method that prints.
// An example written for this post (not Spring's code)
@Configuration
static class EmptyConfig {
}
@Configuration
static class OneMappingConfig {
@Bean
BeanNameUrlHandlerMapping myMapping() {
return new BeanNameUrlHandlerMapping();
}
}
static void show(String title, Class<?> config) throws Exception {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(config);
DispatcherServlet servlet = new DispatcherServlet(context);
servlet.init(new MockServletConfig(new MockServletContext(), "dispatcher"));
System.out.println("== " + title);
System.out.println(" HandlerMapping beans in the context: "
+ context.getBeanNamesForType(HandlerMapping.class).length);
System.out.println(" HandlerMappings the servlet uses:");
for (HandlerMapping mapping : servlet.getHandlerMappings()) {
System.out.println(" - " + mapping.getClass().getSimpleName());
}
System.out.println(" MultipartResolver: " + servlet.getMultipartResolver());
servlet.destroy();
}
== no strategy beans
HandlerMapping beans in the context: 0
HandlerMappings the servlet uses:
- BeanNameUrlHandlerMapping
- RequestMappingHandlerMapping
- RouterFunctionMapping
MultipartResolver: null
== one HandlerMapping bean
HandlerMapping beans in the context: 1
HandlerMappings the servlet uses:
- BeanNameUrlHandlerMapping
MultipartResolver: null
This example too was run on Spring Framework 7.0.9 (7.1.0-SNAPSHOT was not built and run directly).
- With no beans, the three from the properties file come in, in that order. The context has 0
HandlerMappingbeans, yet the servlet is using three. Here you can see that default strategy objects are not registered as beans. - Registering one bean removes all three defaults. The
if (this.handlerMappings == null)in the code above means "only when none were found". The defaults are not added to what you registered. MultipartResolverisnull. When there is no bean,initMultipartResolver()does not look for a default and leaves itnull(lines 467–473).
Easy to get wrong
- "I added one and now
@RequestMappingstopped working." Point 2 of Example 2 is the cause. If you register aHandlerMappingorHandlerAdapterbean yourself, the default implementations that process@RequestMapping(RequestMappingHandlerMapping,RequestMappingHandlerAdapter) drop out with the rest. The class Javadoc says in bold that in that case you must define those two yourself as well (lines 128–134). - The method comments disagree with the properties file. The comment on
initHandlerMappings()says the default isBeanNameUrlHandlerMapping(lines 503–504), the one oninitHandlerExceptionResolvers()says there is no default (lines 588–589), and the one oninitFlashMapManager()saysDefaultFlashMapManager(lines 692–693). What is actually used is what the properties file above says (three mappings, three exception resolvers,SessionFlashMapManager). When a comment and the code disagree, follow the code. - This file is not there for you to edit. Its header says it is "not meant to be customized by application developers" (lines 1–3). To change a strategy, register a bean. In real projects it is usually
@EnableWebMvcor Spring Boot that registers these parts as beans; the inside of that configuration is not covered here (Spring Boot is a separate project from this repository, so the Spring Boot part is not something checked in the source).
4. When a request arrives ①: what happens before doDispatch
Now the request. When the servlet container calls service(), the call passes through two layers before it reaches doDispatch().
Yellow boxes are the places where "something about this request is put somewhere". Whatever is put there is always taken down again.
FrameworkServlet.processRequest() — set the thread holders, then clear them
FrameworkServlet.service() passes the request up to HttpServlet.service() if its HTTP method is one of the eight standard ones (DELETE, HEAD, GET, OPTIONS, PATCH, POST, PUT, TRACE — lines 172–173), and calls processRequest() right away otherwise (lines 868–877). As section 1 showed, what is passed up also ends in processRequest() by way of doGet() and friends.
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// ...
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
// ...
initContextHolders(request, localeContext, requestAttributes);
try {
doService(request, response);
}
// ...
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java lines 983–1020
The doService(request, response) in the middle is the slot for the layer below; what surrounds it is this layer's job.
- Before: it puts this request's data into
LocaleContextHolderandRequestContextHolder(initContextHolders, lines 1055–1064). Both areThreadLocals, that is, storage that each thread has separately (LocaleContextHolderlines 48–52,RequestContextHolderlines 50–54). A servlet handles one request on one thread, so once the data is there, any code handling that request can get "the current request" and "the current locale" without having them passed in as arguments. - After (
finally): it restores both holders to the values they had on entry (resetContextHolders, lines 1066–1071). This runs whether the request succeeds or throws. Threads are reused for the next request, so if nothing were cleared, data from one request would leak into the next. Then it signals that the request is complete (requestCompleted()) and publishes aServletRequestHandledEventto the context (lines 1127–1140).
The catch blocks cut out rethrow whatever doService() threw, either as is or wrapped in ServletException("Request processing failed: …") (lines 1003–1010). You will see that message in Example 3.
DispatcherServlet.doService() — what goes into request attributes
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
// ...
try {
doDispatch(request, response);
}
finally {
// ...
}
}
spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java lines 829–879
Before calling doDispatch(), doService() puts a few things into request attributes (request.setAttribute). Request attributes are a name-value store attached to the request object, readable by any code that receives that request.
| Attribute name (constant) | What is stored | Where |
|---|---|---|
WEB_APPLICATION_CONTEXT_ATTRIBUTE |
this servlet's context | line 847 |
LOCALE_RESOLVER_ATTRIBUTE |
the LocaleResolver |
line 848 |
INPUT_FLASH_MAP_ATTRIBUTE |
values handed over by the previous request (if any) | line 853 |
OUTPUT_FLASH_MAP_ATTRIBUTE |
an empty FlashMap for values to hand to the next request |
line 855 |
FLASH_MAP_MANAGER_ATTRIBUTE |
the FlashMapManager |
line 856 |
The values of the constants are the class name of DispatcherServlet with a suffix such as .CONTEXT (lines 208–241). The reason for storing them is, as the comment says, to "make framework objects available to handlers and view objects" (line 846). Handlers and views do not know the DispatcherServlet object, but they do receive the request object, so these things travel on the request. For example, RequestContextUtils.findWebApplicationContext() looks at this attribute first (RequestContextUtils lines 83–97).
Easy to get wrong: there are two kinds of storage
- The thread holders (
RequestContextHolder,LocaleContextHolder) are set and cleared byFrameworkServlet. They are for code that does not have the request object at hand. - The request attributes are set by
DispatcherServlet. They are for code that does have the request object at hand.
The thread holders are only visible on that thread. If a handler passes work to another thread, they are empty there (threadContextInheritable defaults to false — line 205).
5. When a request arrives ②: one pass through doDispatch()
This is the center of the post. doDispatch() is 70 lines long (lines 935–1004), and its skeleton is this.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Determine handler adapter and invoke the handler.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
// ...
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
// ...
finally {
// ...
}
}
spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java lines 935–1004
Blue boxes are the three places interceptors are called; red boxes are the two places where processing ends without reaching a handler.
A normal request takes seven steps.
| Step | Code | What it does | Post that goes inside |
|---|---|---|---|
| ① | checkMultipart(request) |
unpacks the request if it is a file upload | — |
| ② | getHandler(processedRequest) |
gets the handler together with its interceptors | Part 22 |
| ③ | mappedHandler.applyPreHandle(...) |
interceptor preHandle |
Part 23 |
| ④ | getHandlerAdapter(...), then ha.handle(...) |
calls the handler; a ModelAndView comes back |
Part 22 |
| ⑤ | mappedHandler.applyPostHandle(...) |
interceptor postHandle |
Part 23 |
| ⑥ | processDispatchResult(...) |
handles an exception if there is one, renders a view if there is one | Part 23 |
| ⑦ | mappedHandler.triggerAfterCompletion(...) |
interceptor afterCompletion (called at the end of ⑥) |
Part 23 |
A ModelAndView is the container for a handler's result: "which screen (view) to draw with which data (model)". If the handler has already written the whole response itself, null comes back and there is no screen to draw. Today's controllers that return JSON are mostly this case.
② and ④: ask in turn, use the first one that answers
protected @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
// ...
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java lines 1154–1195
The two methods have the same shape. They walk the list collected in section 3 from the front and use the first one that answers. This is why the lists were sorted in section 3.
getHandler()asks eachHandlerMapping"do you have a handler for this request?" (mapping.getHandler(request)—HandlerMappingline 187) and returns the first answer that is notnull.getHandlerAdapter()asks eachHandlerAdapter"do you know how to call this handler?" (adapter.supports(handler)—HandlerAdapterline 62) and returns the first one that saystrue. If none does, it is aServletException.
What getHandler() returns is not the handler itself but a HandlerExecutionChain: an object that bundles one handler with the list of interceptors to apply to this request (HandlerExecutionChain lines 44–48). So doDispatch() never deals with interceptors directly; it tells this bundle to applyPreHandle, applyPostHandle and triggerAfterCompletion.
③⑤⑦: the order of interceptors
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)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}
// ...
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception in interceptor [" + interceptor + "]", ex2);
}
}
}
spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExecutionChain.java lines 142–181
preHandleis called from the front (starting ati = 0). Each time one returnstrue, its index is recorded ininterceptorIndex.postHandleis called from the back (lines 157–164).afterCompletionis also called from the back, but starting atinterceptorIndex. That means only the interceptors that passedpreHandleare called.
If a preHandle returns false, triggerAfterCompletion is called on the spot and false is returned; doDispatch() sees that and returns immediately (lines 957–959). The handler is not called. As the comment puts it, DispatcherServlet "assumes that this interceptor has already dealt with the response itself" (lines 138–140). An interceptor that blocks requests from users who are not logged in takes this route.
preHandle runs front to back, the other two back to front, and afterCompletion only reaches interceptors whose preHandle passed. The order comes from the real output of Example 3 below.
⑥: processDispatchResult() — where exceptions and views meet
The inner try of doDispatch() does not throw an exception raised while finding and calling the handler; it keeps it in a variable (dispatchException = ex, lines 972–974). Then it passes the normal result (mv) and the exception together to processDispatchResult().
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
// ...
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException mavDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = mavDefiningException.getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
// ...
}
// ...
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java lines 1022–1062
- If there is an exception,
processHandlerException()asks theHandlerExceptionResolvers collected in section 3, one after another (lines 1225–1232). If one answers, the answer becomesmv; if none does, the exception is thrown again (line 1255). - If there is something to draw,
render()asks theViewResolvers to turn the view name into a view object and has that view draw (lines 1267–1313). - At the end it calls
afterCompletion. The exception has already been dealt with, so the argument isnull.
The exception rethrown in step 1 is caught by the outer catch of doDispatch(), which calls triggerAfterCompletion(…, ex). This time the exception is passed as the argument, and at the end that exception is thrown yet again (lines 1357–1364). Whichever route is taken, afterCompletion is called on the interceptors that passed preHandle.
An exception handled by a resolver rejoins the normal path; an unhandled one leaves with the exception passed to afterCompletion. The grey boxes are the real results of Example 3 below.
When there is no handler — noHandlerFound()
If no HandlerMapping answers in step ②, mappedHandler is null.
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (pageNotFoundLogger.isWarnEnabled()) {
pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));
}
throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
new ServletServerHttpRequest(request).getHeaders());
}
spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java lines 1172–1178
It logs a warning and throws NoHandlerFoundException. This exception too is raised inside the inner try, so it lands in dispatchException and goes to processDispatchResult(). In other words, "no handler" is handled by a HandlerExceptionResolver just like any other exception. NoHandlerFoundException knows that its status code is 404 (NoHandlerFoundException line 65), and among the default resolvers DefaultHandlerExceptionResolver calls response.sendError() with that code (lines 221–232, lines 556–577). Since there is no mappedHandler here, no interceptor is called at all.
Where an async request leaves early
Right after step ④ there is this.
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java lines 965–967
This is the case where the handler returned saying "the result will come later, from another thread" (isConcurrentHandlingStarted() — WebAsyncManager lines 145–147). There is no result yet, so it leaves without postHandle, without rendering, and without afterCompletion. Instead, finally calls applyAfterConcurrentHandlingStarted(), with a comment that reads "Instead of postHandle and afterCompletion" (lines 990–996). When the result is ready, the request enters DispatcherServlet once more to finish the rest. That second pass is not covered in this post.
Example 3: logging in interceptors to see the order
We register a BeanNameUrlHandlerMapping with three interceptors (A, B, C) and three handlers, and push four requests straight into servlet.service(). The handlers use the simplest shape there is, HttpRequestHandler (one method taking the request and the response). BeanNameUrlHandlerMapping treats a bean name starting with / as a URL (BeanNameUrlHandlerMapping lines 54–66). Interceptor B returns false from preHandle only for the /blocked request. The full code is ex/u19/DispatchOrderExample.java; below are the configuration class and main (LogInterceptor, which just prints its name in the three methods, is left out).
// An example written for this post (not Spring's code)
@Configuration
static class Config {
// BeanNameUrlHandlerMapping treats a bean name starting with "/" as a URL
@Bean("/hello")
HttpRequestHandler hello() {
return (request, response) -> {
Object context = request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
System.out.println(" handler /hello runs");
System.out.println(" context in request attribute: " + (context != null));
System.out.println(" RequestContextHolder is set: "
+ (RequestContextHolder.getRequestAttributes() != null));
response.getWriter().write("hello");
};
}
@Bean("/boom")
HttpRequestHandler boom() {
return (request, response) -> {
System.out.println(" handler /boom runs and throws");
throw new IllegalStateException("boom");
};
}
@Bean("/blocked")
HttpRequestHandler blocked() {
return (request, response) -> System.out.println(" handler /blocked runs (must not appear)");
}
@Bean
BeanNameUrlHandlerMapping handlerMapping() {
BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping();
mapping.setInterceptors(new LogInterceptor("A", false), new LogInterceptor("B", true),
new LogInterceptor("C", false));
return mapping;
}
@Bean
ApplicationListener<ServletRequestHandledEvent> handledListener() {
return event -> System.out.println(" [event] ServletRequestHandledEvent url=" + event.getRequestUrl()
+ " status=" + event.getStatusCode() + " failed=" + event.wasFailure());
}
}
public static void main(String[] args) throws Exception {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(Config.class);
DispatcherServlet servlet = new DispatcherServlet(context);
servlet.init(new MockServletConfig(new MockServletContext(), "dispatcher"));
for (String path : new String[] {"/hello", "/blocked", "/boom", "/nothing"}) {
System.out.println("GET " + path);
MockHttpServletRequest request = new MockHttpServletRequest("GET", path);
MockHttpServletResponse response = new MockHttpServletResponse();
try {
servlet.service(request, response);
System.out.println(" => status=" + response.getStatus() + ", body=\"" + response.getContentAsString()
+ "\", errorMessage=" + response.getErrorMessage());
}
catch (ServletException ex) {
System.out.println(" => exception out of the servlet: " + ex.getMessage());
}
System.out.println(" RequestContextHolder after service: " + RequestContextHolder.getRequestAttributes());
}
servlet.destroy();
}
GET /hello
preHandle A -> true
preHandle B -> true
preHandle C -> true
handler /hello runs
context in request attribute: true
RequestContextHolder is set: true
postHandle C (ModelAndView=null)
postHandle B (ModelAndView=null)
postHandle A (ModelAndView=null)
afterCompletion C (ex=null)
afterCompletion B (ex=null)
afterCompletion A (ex=null)
[event] ServletRequestHandledEvent url=/hello status=200 failed=false
=> status=200, body="hello", errorMessage=null
RequestContextHolder after service: null
GET /blocked
preHandle A -> true
preHandle B -> false
afterCompletion A (ex=null)
[event] ServletRequestHandledEvent url=/blocked status=200 failed=false
=> status=200, body="", errorMessage=null
RequestContextHolder after service: null
GET /boom
preHandle A -> true
preHandle B -> true
preHandle C -> true
handler /boom runs and throws
afterCompletion C (ex=java.lang.IllegalStateException: boom)
afterCompletion B (ex=java.lang.IllegalStateException: boom)
afterCompletion A (ex=java.lang.IllegalStateException: boom)
[event] ServletRequestHandledEvent url=/boom status=200 failed=true
=> exception out of the servlet: Request processing failed: java.lang.IllegalStateException: boom
RequestContextHolder after service: null
GET /nothing
[main] WARN org.springframework.web.servlet.PageNotFound - COMMONS-LOGGING No mapping for GET /nothing
[main] WARN org.springframework.web.servlet.PageNotFound - COMMONS-LOGGING No endpoint GET /nothing.
[event] ServletRequestHandledEvent url=/nothing status=404 failed=false
=> status=404, body="", errorMessage=No endpoint GET /nothing.
RequestContextHolder after service: null
This example too was run on Spring Framework 7.0.9 (7.1.0-SNAPSHOT was not built and run directly). The two lines starting with [main] WARN were not printed by the example; they are logs written by Spring. They go to standard error while the other lines go to standard output. Because those are two separate streams, where the two lines land when both are shown on one screen is not guaranteed. The listing above is the arrangement from an actual run; in another environment they may appear elsewhere.
/hello— normal.preHandlegoes A→B→C;postHandleandafterCompletiongo C→B→A. Inside the handler, the context is in the request attribute andRequestContextHolderis set (section 4).ModelAndViewisnullbecause the adapter forHttpRequestHandlerassumes the handler wrote the response itself and returnsnull(HttpRequestHandlerAdapterlines 47–52)./blocked— B blocks. Neither C'spreHandle, nor the handler, norpostHandleis called, and only A getsafterCompletion, becauseinterceptorIndexstopped at 0 (A). The response status is 200. The interceptor that blocks is supposed to write the response itself, and B in this example wrote nothing./boom— the handler throws.postHandleis skipped and all three getafterCompletionwith the exception as argument. None of the three default resolvers takes anIllegalStateException, so the exception was thrown again, andprocessRequest()wrapped it in aServletExceptionand let it out of the servlet. That is thefailed=truein the event. On a real server the servlet container receives this exception and produces an error response./nothing— no handler. No interceptor is called and the result is 404. The exception did not come out becauseDefaultHandlerExceptionResolverhandled it.- After
service()returns,RequestContextHolderis back tonullevery time, even for/boom, which threw. That is thefinallyofprocessRequest().
Easy to get wrong
postHandleis not called when there is an exception.applyPostHandleis the last line of the innertry, so an exception before it skips it. Anything that must be cleaned up no matter what belongs inafterCompletion.- A
nullexinafterCompletiondoes not mean there was no exception. If aHandlerExceptionResolverhandled the exception,afterCompletionreceivesnull(lines 1058–1061)./boomin the example is the case where nobody handled it, so the exception came through. - The interceptor whose
preHandlereturnedfalsedoes not getafterCompletionitself.interceptorIndexonly advances aftertrueis returned. - In this commit
noHandlerFound()always throws. The method has no branch that sends a 404 directly. Because it is unified as an exception, "no handler" can be handled along the same route as any other error. - Where
PATCHis handled depends on the version. This commit has adoPatch()whose Javadoc carries@since 7.1(lines 893–903).FrameworkServletin 7.0.9, on which the examples ran, has nodoPatch()(checked against thev7.0.9tag of the repository). Either way, everything converges onprocessRequest().
What this post left out
Names and locations only. logRequest(), which logs the request, and enableLoggingRequestDetails, which masks sensitive values (lines 881–922). The part that snapshots request attributes for an include request and restores them afterwards (lines 832–844, and from line 1371). parseRequestPath, which parses the path ahead of time (lines 859–863). doOptions() and doTrace(), which decide whether to dispatch OPTIONS and TRACE requests (lines 944–976). checkMultipart() and cleanupMultipart(), which unpack and clean up multipart requests (lines 1089–1146). applyDefaultViewName(), which supplies a view name when there is none (lines 1009–1016). buildLocaleContext(), which determines the locale through the LocaleResolver (lines 1072–1080). RequestBindingInterceptor, which sets the thread holders again during async processing (lines 1192–1210). ApplicationContextInitializer and applyInitializers(), which adjust the context before refresh (from line 740). And the inside of HandlerMapping and HandlerAdapter (Part 22) and of HandlerExceptionResolver, interceptor registration and ViewResolver (Part 23).
Summary
| Concept | One-line summary |
|---|---|
| Four layers | HttpServlet → HttpServletBean (settings) → FrameworkServlet (context, thread holders) → DispatcherServlet (strategies, dispatching) |
init() |
turns init-params into properties, then initServletBean() → initWebApplicationContext() |
| The servlet's context | given to the constructor, found in an attribute, or newly created. The root context, if any, becomes the parent |
onRefresh() |
called on receiving the context's ContextRefreshedEvent; called directly if no event arrived |
initStrategies() |
finds eight kinds of parts in the context and keeps them in fields: four by name (one each), four by type (several) |
DispatcherServlet.properties |
the default implementations when there are no beans at all. Register even one and the defaults of that kind are all gone |
processRequest() |
sets LocaleContextHolder and RequestContextHolder, restores them in finally, then publishes an event |
doService() |
puts the context, LocaleResolver and FlashMap into request attributes and calls doDispatch() |
doDispatch() |
getHandler → applyPreHandle → ha.handle → applyPostHandle → processDispatchResult → afterCompletion |
HandlerExecutionChain |
one handler + a list of interceptors. preHandle from the front, the other two from the back |
afterCompletion |
called only on interceptors that passed preHandle, and called even when an exception was thrown |
processDispatchResult() |
exceptions go to a HandlerExceptionResolver, views to render(). If nobody takes the exception it is thrown again |
| No handler | throws NoHandlerFoundException and handles it like any other exception. The default result is 404 |
| Async | if the handler started async processing, it leaves early without postHandle or afterCompletion |
What's next
Throughout this post DispatcherServlet only ever asked: "who takes this?" to the HandlerMappings, "can you call it?" to the HandlerAdapters. The next post is about the side that gets asked. It looks at when and how methods annotated with @RequestMapping are registered and matched to requests, and how RequestMappingHandlerAdapter fills in a method's arguments (@RequestParam, @RequestBody, ...) and turns its return value into a response.
- Next: Digging into Spring's Source 22: Handler Mappings and Adapters — How @RequestMapping Is Found and Invoked
- After that: Part 23: Exception Handling, Interceptors, Views — Before, After and at the End of a Request
- Previous: Part 20: The spring-web Foundation — HttpMessageConverter, WebApplicationContext, Filters
- Posts this one builds on: Part 7: The Twelve Steps of refresh(), Part 12: Events, Environment, Resource, Part 2: The Big Map — Modules and Packages
- The start of the series: Part 1: How I Read It with AI, and the Map
- How a web server written in C walks one request through its phases: Digging into Apache httpd's Source 10: Request Processing — From a URL to a File and a Handler
- How Tomcat gets to calling DispatcherServlet's
service(): Digging into Tomcat's Source 9: The Filter Chain and the Servlet Call — Until service() Runs