namalzi

← All posts

Digging into Spring's Source 21: DispatcherServlet — How One Request Becomes a Response

Spring · Published

We follow a single HTTP request through Spring MVC by reading the source of DispatcherServlet. Written for people new to Spring, it covers how the servlet sets up its own context and strategy objects at startup, and the order in which doDispatch finds a handler, calls it, and weaves in interceptors and exception handling.

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.

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.

The inheritance chain of DispatcherServlet. At the top is HttpServlet from the Servlet API, whose service method calls doGet, doPost and so on by HTTP method. Below it, HttpServletBean copies servlet init-params onto its own properties in init. FrameworkServlet sets up its own context in initServletBean and sets and clears the thread holders per request in processRequest. At the bottom, DispatcherServlet collects the strategy objects in onRefresh and sends the request to a handler in doService and doDispatch. 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

2. At startup: the servlet sets up its own context

When the servlet container calls init(), the call travels down through three layers.

The servlet initialization flow. The servlet container calls init; HttpServletBean.init turns init-params into properties and calls FrameworkServlet.initServletBean. initWebApplicationContext checks whether there is a context already. If one was given to the constructor and is not refreshed yet, configureAndRefreshWebApplicationContext adds a listener and calls refresh; if there is none, createWebApplicationContext creates a new one and takes the same path. ContextRefreshedEvent is published in the last step of refresh, and onRefresh is called on receiving it. If the context was already refreshed, no event arrives, so onRefresh is called directly. onRefresh leads to DispatcherServlet.initStrategies, which collects eight kinds of strategy objects. 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:

  1. Look up the root context. rootContext is a context shared by the whole application that may already be running before the servlet. If there is none it is null. Who starts it is covered in Part 20.
  2. If a context was given to the constructor, use it. That is the new DispatcherServlet(context) case (DispatcherServlet lines 366–369). If it has not been refreshed yet, the root context becomes its parent when it has none, and configureAndRefreshWebApplicationContext() goes on to refresh it.
  3. Otherwise, look in the servlet context attributes. This only applies when contextAttribute has been set (lines 615–626).
  4. If there still is none, create one. createWebApplicationContext(rootContext) instantiates the context class, sets the root context as parent, and calls the same configureAndRefreshWebApplicationContext() as in step 2 (lines 643–663). The context class used when nothing else is configured is XmlWebApplicationContext (line 155).
  5. 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

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.

  1. In [1], init() refreshed the context, and onRefresh() was called via the event. Before init() the context is inactive and getHandlerMappings() is null.
  2. The servlet name first appears in the context id and in the servlet context attribute name. The id is assigned by configureAndRefreshWebApplicationContext() (lines 666–677), and the attribute is set at the end of initWebApplicationContext() (the publishContext part cut out above, lines 596–600) under FrameworkServlet.CONTEXT. plus the servlet name (lines 789–791).
  3. 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.
  4. destroy() closed both contexts. destroy() leaves the context open only when it was injected through setApplicationContext() (lines 852–860). For a context passed to the constructor, webApplicationContextInjected stays false (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

  1. beansOfTypeIncludingAncestors(...) finds every bean of type HandlerMapping in this context and its parent contexts. Ones placed in the root context are found too.
  2. If any were found, they go into a list and are sorted by order, the order given by @Order or Ordered. That order becomes "who gets asked first" in section 5.
  3. 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

spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties lines 5–25

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).

The order of looking up one strategy object. If the context has such a bean, that bean is used, sorted by order if there are several. If not, check whether it is the MultipartResolver. If so, it is left null and there is no multipart handling; if not, the implementation class names for the interface name are read from DispatcherServlet.properties and created with createBean. Objects created this way are not registered as beans. Either way the result is kept in a field of the servlet. 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).

  1. With no beans, the three from the properties file come in, in that order. The context has 0 HandlerMapping beans, yet the servlet is using three. Here you can see that default strategy objects are not registered as beans.
  2. 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.
  3. MultipartResolver is null. When there is no bean, initMultipartResolver() does not look for a default and leaves it null (lines 467–473).

Easy to get wrong

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().

The layers a request passes before reaching doDispatch. When a request arrives, FrameworkServlet.service sends standard HTTP methods on to HttpServlet.service, and doGet, doPost, doPut, doDelete and doPatch are each one line calling processRequest. processRequest puts this request into LocaleContextHolder and RequestContextHolder. DispatcherServlet.doService puts the context, LocaleResolver and FlashMap into request attributes. doDispatch finds a handler and calls it. When it is over, the finally block of processRequest restores both holders to their previous values and publishes ServletRequestHandledEvent. 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.

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 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

The flow of doDispatch. After checkMultipart, getHandler asks each HandlerMapping in turn. If nobody knows, noHandlerFound throws NoHandlerFoundException, which is caught and goes to processDispatchResult. If a HandlerExecutionChain comes back, applyPreHandle calls the interceptors' preHandle from first to last. If one returns false, processing stops there and only the interceptors that passed get afterCompletion. If true, getHandlerAdapter and then ha.handle run the handler and return a ModelAndView. If async handling has started, it leaves early without postHandle. Otherwise applyPostHandle calls the interceptors' postHandle from last to first, and processDispatchResult hands an exception to a HandlerExceptionResolver if there was one and renders if there is a view. An exception thrown by the handler also goes to processDispatchResult. Finally triggerAfterCompletion calls the interceptors' afterCompletion from last to first. 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.

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

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.

The three requests of Example 3, with interceptors A, B and C. GET /hello (normal): preHandle returns true for A, B and C in that order, so interceptorIndex becomes 2; after handler /hello runs, postHandle goes C, B, A and afterCompletion goes C, B, A with ex=null. GET /blocked (B returns false): preHandle A returns true, so interceptorIndex is 0; preHandle B returns false, so C, the handler and postHandle are skipped, and afterCompletion runs backwards from interceptorIndex, so only A gets it. GET /boom (the handler throws): preHandle returns true for A, B and C, handler /boom throws IllegalStateException, postHandle is skipped, and afterCompletion goes C, B, A with ex=IllegalStateException: boom 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

  1. If there is an exception, processHandlerException() asks the HandlerExceptionResolvers collected in section 3, one after another (lines 1225–1232). If one answers, the answer becomes mv; if none does, the exception is thrown again (line 1255).
  2. If there is something to draw, render() asks the ViewResolvers to turn the view name into a view object and has that view draw (lines 1267–1313).
  3. At the end it calls afterCompletion. The exception has already been dealt with, so the argument is null.

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.

The inner try of doDispatch puts any exception thrown while finding or calling the handler into dispatchException (lines 972–974); so does NoHandlerFoundException when there is no handler. processDispatchResult(mv, dispatchException), when there is no exception, renders the view through the ViewResolvers with render() if there is an mv. When there is an exception, processHandlerException asks the HandlerExceptionResolvers in turn; if one answers, that answer becomes mv and takes the same render path. Then triggerAfterCompletion(…, null) runs — the exception is already handled. Example 3's /nothing took this path, handled by DefaultHandlerExceptionResolver into a 404. If nobody answers, the exception is rethrown (line 1255) and the outer catch calls triggerAfterCompletion(…, ex), passing the exception to afterCompletion and throwing it again (lines 1357–1364). Example 3's /boom took this path: no resolver takes IllegalStateException, so the exception left the servlet 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.

  1. /hello — normal. preHandle goes A→B→C; postHandle and afterCompletion go C→B→A. Inside the handler, the context is in the request attribute and RequestContextHolder is set (section 4). ModelAndView is null because the adapter for HttpRequestHandler assumes the handler wrote the response itself and returns null (HttpRequestHandlerAdapter lines 47–52).
  2. /blocked — B blocks. Neither C's preHandle, nor the handler, nor postHandle is called, and only A gets afterCompletion, because interceptorIndex stopped 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.
  3. /boom — the handler throws. postHandle is skipped and all three get afterCompletion with the exception as argument. None of the three default resolvers takes an IllegalStateException, so the exception was thrown again, and processRequest() wrapped it in a ServletException and let it out of the servlet. That is the failed=true in the event. On a real server the servlet container receives this exception and produces an error response.
  4. /nothing — no handler. No interceptor is called and the result is 404. The exception did not come out because DefaultHandlerExceptionResolver handled it.
  5. After service() returns, RequestContextHolder is back to null every time, even for /boom, which threw. That is the finally of processRequest().

Easy to get wrong

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.

More posts