In-depth analysis of Spring source code: Automatic registration mechanism of DispatcherServlet

learning path: SPI mechanism → Spring implementation → Inheritance chain → Registration process → Spring Boot differences → Best practices Supporting source code: gitee.com/chouchouxia…


1. Background knowledge: Evolution of Servlet container startup process

When building Java web applications, the first problem developers often face is how to make the request entry component correctly recognized by the web container.

Servlet 2.x era (web.xml era):

  • All Servlets, Filters, and Listeners must be in web.xml Explicit declaration in
  • Every time you add a function, you need to modify the deployment descriptor, which has a high degree of coupling.
  • Framework integration requires writing a lot of templated XML

Servlet 3.0+ (zero XML/programmatic registration era):

  • introduce javax.servlet.ServletContainerInitializer SPI mechanism
  • When the container starts, it automatically scans all jar packages under the classpath. META-INF/services/ Table of contents
  • The framework can programmatically register Servlet/Filter/Listener with the container
  • Completely break away from XML and embrace type safety

Programmatic registration API introduced in Servlet 3.0:

// Servlet register API
ServletRegistration.Dynamic reg = servletContext.addServlet("name", servletInstance);
reg.addMapping("/path");                 // URL mapping
reg.setLoadOnStartup(1);                 // Load on startup(>=0 Indicates initialization when the container starts)
reg.setAsyncSupported(true);             // Asynchronous support
reg.setInitParameter("key", "value");    // Initialization parameters

// Filter register API
FilterRegistration.Dynamic filterReg = servletContext.addFilter("name", filterInstance);
filterReg.addMappingForUrlPatterns(null, true, "/path/*");

// Listener register API
servletContext.addListener(listenerInstance);

1.0 Questions and Answers on Servlet Basic Concepts

Before diving into the source code, we need to answer a few basic but crucial questions. These issues run through the entire design philosophy of Servlet and Spring MVC.


Q1: Why do we need Servlet? What exactly is done in Servlet?

Servlet is the core cornerstone of Java Web, which solvesHTTP is a stateless protocol, but we need stateful business processingThis question.

To put it simply: the browser sends an HTTP request (a string of text), and the Servlet containerParse into Java object, let you use Java code to process it, and then put the resultConvert back to HTTP responseSend it back.

The core process inside Servlet has only 4 steps:

stepcomponentswhat did
①ReceiveHttpServletRequestParse HTTP requests into objects: URL, parameters, Header, Cookie, Body
② Processingyour doGet()/doPost()Java business logic (query database, call Service, spell results)
③ResponseHttpServletResponseSet status code, header, and write content to the output stream
④PackagingServlet container (Tomcat)Bundle resp The object is serialized into a standard HTTP response message and sent back

in the project HelloServlet For example (Supporting source code):

// 1. Container call service() → according to HTTP method is distributed to doGet()
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    // 2. Set response headers
    resp.setContentType("text/html;charset=UTF-8");
    // 3. Get the output stream and write HTML
    PrintWriter out = resp.getWriter();
    out.println("<h1>Hello World</h1>");
    // 4. After the stream is closed, the container packages the contents into HTTP Response sent back to browser
}

If there is no Servlet, you need to manually parse the original HTTP message in Java:

GET /api/info HTTP/1.1
Host: localhost:8080
Accept: application/json

You have to write Sockets yourself, parse strings, handle encoding, manage thread pools, and follow HTTP specifications - all the dirty work is done for you by the Servlet container.

What does DispatcheServlet do on top of this? It is essentially aSuper Servlet, splitting request processing into a set of pipelines:

ask → HandlerMapping(Which one to look for Controller)
     → HandlerAdapter(call Controller method)
     → Controller execute (your @RequestMapping method)
     → ViewResolver(try to find JSP / JSON view)
     → response

Spring MVC does not "replace" Servlets;Standing on the shoulders of ServletsCreate a higher-level abstraction so that you don’t have to write doGet(),Write @RequestMapping That's enough.

Extension: What is the difference between DispatcherServlet and Servlet?

Servlet is an interface specification, and DispatcherServlet is a specific implementation of it by Spring. Looking at the inheritance chain:

Servlet (interface — Java EE specification)
  └── GenericServlet (Abstract class, protocol independent)
        └── HttpServlet (abstract class, HTTP protocol)
              └── HttpServletBean (Spring access point)
                    └── FrameworkServlet (Spring Container aware)
                          └── DispatcherServlet ★

The core difference is one sentence:Servlet only gives you an empty one doGet()/doPost(), you have to write the rest yourself; DispatcherServlet builds a "distribution factory" on top of Servlet to automatically route requests to @Controller.

DimensionsOrdinary ServletDispatcherServlet
How to handle requestsYou are here by yourself doGet()/doPost() Hardly written logicThe request enters → HandlerMapping finds the processor → HandlerAdapter calls @Controller method → ​​return response
URL routing@WebServlet("/user"), a Servlet corresponds to a fixed path@RequestMapping("/user/{id}"), supports path variables and RESTful style
Parameter bindingManual req.getParameter("name")Automatically bind request parameters to method parameters (@RequestParam, @PathVariable, @RequestBody)
response handlingManual resp.getWriter().print(json)Method returns object → HttpMessageConverter Automatically convert to JSON/XML
ScalabilityNo built-in extension pointsHandlerMapping / HandlerAdapter / Interceptor / ViewResolver are fully pluggable

Simple analogy:

// ordinary Servlet: Handmade workshop, do every step yourself
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    String id = req.getParameter("id");        // Manually take parameters
    User user = userService.findById(id);       // Manual adjustment Service
    resp.setContentType("application/json");
    resp.getWriter().write(toJson(user));       // Manual serialization JSON
}

// DispatcherServlet: You only write about the business, and the rest of the framework is covered
@GetMapping("/user/{id}")
@ResponseBody
public User getUser(@PathVariable Long id) {    // Automatic parameter binding
    return userService.findById(id);            // Just return the object, JSON Automatically generated
}

So DispatcherServlet does not "replace" Servlet, butStanding on the shoulders of Servlet to create a higher level abstraction——It is itself a Servlet (inherits HttpServlet), but split the request processing into the HandlerMapping → HandlerAdapter → ViewResolver pipeline, allowing you to write doGet() become write @RequestMapping.


Q2: What does the Servlet container followed by (Tomcat) mean? What is the relationship between Servlet and Tomcat?

Servlet is the interface specification, and Tomcat is the implementer.

conceptanalogy
Servlet specification (javax.servlet.*)USB interface standard (specifies shape, protocol)
TomcatSpecific USB hardware (implements the interface and can be plugged and unplugged)

More specifically:

  • Servlet is just a set of Java interfaces (javax.servlet.Servlet, javax.servlet.ServletContext etc.), stipulates "how to receive requests and how to return responses", but the interface itself cannot do the job.
  • Tomcat implements this set of interfaces and is responsible for the real work: listening to port 8080, receiving HTTP connections, parsing HTTP messages, constructing objects, calling your methods, and serializing responses.

So the Servlet code you write has never seen Socket or handled TCP connections - it is all done by Tomcat behind the scenes.

in the project pom.xml middle:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <scope>provided</scope>  <!-- key: provided -->
</dependency>

scope=provided means:Required during compilation, but not entered. Because Tomcat comes with this implementation, you can just use it when running.

Other Servlet containers include Tomcat:

containerFeatures
TomcatThe most mainstream, lightweight
JettyLighter and suitable for embedding
UndertowWildFly default, high performance
WebLogic / WebSphereEnterprise level, commercial licensing

No matter which container you use, your servlet code will be the same - because it all follows the same javax.servlet Standard interface.


Q3: Constructed in Tomcat HttpServletRequest How to pass it to the Java program I wrote, constructed in my program HttpServletResponse How to pass it to Tomcat?

The essence is onceOrdinary method call parameter passing, write to you void foo(List list) { list.add("hello"); } Exactly the same logic:

Browser ──HTTP──▶ Tomcat ──Javaobject──▶ your Servlet
  ◀──HTTP──                    ◀──Javaobject──

Take it apart and look at the complete process:

Step 1: Tomcat Receive original HTTP message
  GET /hello HTTP/1.1
  Host: localhost:8080

Step 2: Tomcat Internally constructed object
  RequestFacade req  ─── packed URL, Header, parameter
  ResponseFacade resp ─── Empty, waiting for you to fill it in

Step 3: Tomcat Call your code (an ordinary Java method call)
  helloServlet.service(req, resp);
    └─→ doGet(req, resp) {
          resp.setContentType("text/html");   // Past resp written in Header
          resp.getWriter().write("<h1>...</h1>");  // Past resp written in Body
        }

Step 4: your method return back, Tomcat from resp Get content from

Step 5: Tomcat Serialized into HTTP Response message sent back
  HTTP/1.1 200 OK
  Content-Type: text/html;charset=UTF-8
  
  <h1>Hello World</h1>

Why use Facade?

The real object inside Tomcat is org.apache.catalina.connector.Request, but it does not pass the internal object directly to you, but wraps it up RequestFacade(Achieved HttpServletRequest interface):

Tomcat internal:  Request ──→ RequestFacade ──→ your Servlet.doGet(req)
                        (Only expose interface methods)

This serves two purposes:

  1. Safety: You cannot call Request Internal methods of Tomcat, such as request.getConnector().stop() Shut down the server
  2. isolation: If Tomcat upgrades and changes the internal implementation in the future, as long as the interface remains unchanged, your code does not need to be changed.

One picture summarizes the entire data flow:

            Tomcat inside your code(Servlet)
           
   Socket listening port 8080
        │
        ▼
   parse HTTP message
        │
        ▼
   new Request()  ──Package──▶  RequestFacade ──▶  doGet(req, resp)
   new Response() ◀──Package──  ResponseFacade ◀──  (you write resp)
        │
        ▼
   Serialized into HTTP response
        │
        ▼
   Send back to browser

Q4: Why can Tomcat directly call functions in my Java code?

because Tomcat itself is a Java program, and your code and Tomcat run in the same JVM process.

┌─────────────────── same one Java process (JVM) ───────────────────┐
│                                                               │
│   Tomcat own code:                                           │
│   Connector.java  →  "Request received, which one to call Servlet Coming?"        │
│                          │                                     │
│                          │ ordinary Java method call                    │
│                          ▼                                     │
│   your Servlet code:                                           │
│   HelloServlet.doGet(req, resp) { ... }                       │
│                                                               │
└───────────────────────────────────────────────────────────────┘

This is not an IPC call between two separate programs;In the same JVM, class A calls the method of class B. Tomcat finds your code in three steps:

Step 1: Tomcat discovers your class via SPI. in the project MyServletContainerInitializer After being scanned by Tomcat, instantiate it using reflection:

// Tomcat essentially does this: 
AppInitializer initializer = (AppInitializer)
    clazz.getDeclaredConstructor().newInstance();  // reflection instantiation
initializer.onStartup(ctx);  // Ordinary method call

Step 2: Register the Servlet you wrote with Tomcat. exist HelloServletAppInitializer middle:

// you new HelloServlet() Create an object and hand it to Tomcat save up
// This is similar to map.put("hello", myServlet) No essential difference
ServletRegistration.Dynamic reg = ctx.addServlet("hello", new HelloServlet());
reg.addMapping("/hello");

Step 3: When the request comes, Tomcat adjusts your method.

Tomcat Thread receives GET /hello
  → Check internally Map: "/hello" Which one corresponds to Servlet?
  → Find the one you registered before HelloServlet object
  → hello.servlet(req, resp)          ← a normal method call
    → hello.doGet(req, resp)          ← The business logic you wrote
  → return

Analogy: you write a Calculator class, then a Main class to tune it - Tomcat is the one Main, your Servlet is that Calculator, the difference is that Tomcat uses reflection newInstance().


Q5: Why do traditional WAR projects not have startup classes like Spring Boot?

These are two completely different startup modes:

Traditional WAR project (this project)Spring Boot
Who starts firstTomcat starts first, then load your WARyour main() Start first, and then start Tomcat inline
Where is the main method?on Tomcat Bootstrap classin your own startup class
Packing methodwarjar(Fat JAR, embedded Tomcat)

This project pom.xml It says <packaging>war</packaging>, the output after compilation dispatcher-servlet-demo.war. Throw this WAR into Tomcat webapps Under the directory:

$CATALINA_HOME/bin/startup.sh   →   start up Tomcat (Bootstrap.main())
  → scanning webapps down WAR
    → Discover in your project ServletContainerInitializer
      → call onStartup() Register your Servlet

Spring Boot treats this relationshipreverseGot:

of your project main() Start first
  → new SpringApplication().run()
    → Embedded startup Tomcat(Tomcat Becomes a library in your project)
      → Register again DispatcherServlet

Represented graphically:

WAR model:                    Spring Boot model:

┌──Tomcat main process────────┐    ┌──your main()─────────┐
│  Bootstrap.main()     │    │  SpringApplication   │
│    │                  │    │    │                 │
│    ├─ load your WAR ──►│    │    ├─ Embedded Tomcat ◄──│── Tomcat is your dependent library
│    └─ Adjust your Servlet   │    │    └─ register Servlet   │
└───────────────────────┘    └──────────────────────┘

So in traditional WAR projects, the "startup class" can indeed be understood as Tomcat's own main(). In order to make development more convenient, Spring Boot reverses this relationship and allows you to control the startup entry yourself.


Q6: When DispatcherServlet is mapped to /, another ApiServlet maps to /api/* hour,/api/info To whom will the request be handled?

This is a keyPriority rulesquestion. Taking this project as an example, there are two Servlets:

ServletMapping pathRegistration source
DispatcherServlet/Spring MVC(MySpringMvcInitializer)
ApiServlet/api/*Custom SPI (ApiServletAppInitializer)

at the same time HomeController There are also @RequestMapping("/api/info").

when GET /api/info When arriving at Tomcat,Container-level matching rulesAs follows (Servlet specification definition, priority from high to low):

ask: /api/info

Step 1: exact match → Is there any Servlet exactly mapped to "/api/info" ?
  → No

Step 2: Path prefix matching (longest first)→ Is there any Servlet mapped to "/api/*" ?
  → "/api/*" match!→ ApiServlet win ✅

Step 3: extension match → *.jsp wait

Step 4: Default match → "/"(It's your turn only if none of the previous ones hit.)
priorityMatching methodExamplehit requestwho wins
1exact path/api/infoonly /api/infonone
2Path prefix (longest first)/api/*/api/xxx, /api/infoApiServlet
3extension*.jsp/xxx.jspnot applicable
4DefaultServlet/Go back and match all missed requestsDispatcherServlet (not in turn)

in conclusion: /api/* has a higher priority than /, /api/info In the second step, it was ApiServlet Intercepted,No chance to reach DispatcherServlet at all. so HomeController.info() of @RequestMapping("/api/info") is never called.

this meansThe container first decides which Servlet will handle the request, and then Spring MVC can do internal routing.. DispatcherServlet can handle only those requests that are not intercepted by more precise servlets. This is something you must pay attention to when designing your project - the mapping paths of multiple Servlets should not conflict with each other.


Q7: In Controller @RequestMapping Essentially another Servlet?

no. @RequestMapping It's just an annotation tag on a normal Java method, not a Servlet at all.

by HomeController For example:

@Controller
public class HomeController {          // ← No extends HttpServlet

    @RequestMapping("/")              // ← Just an annotation tag
    public String home(Model model) {
        return "home";
    }
}

HomeController not implemented HttpServlet,No service(req, resp) method, Tomcat doesn't recognize it at all. In the entire project, Tomcat only knows that implementation HttpServlet The entrance to — DispatcherServlet.

The actual call chain is like this:

ask GET /
  │
  ▼
Tomcat: "match '/' → turn up DispatcherServlet(because it maps to /)"
  │
  ▼
DispatcherServlet.doDispatch()         ← ★ This is the only step Servlet
  │
  ├── get req, resp(Tomcat passed in)
  ├── HandlerMapping: "The request path is /, who handles?→ HomeController.home()"
  ├── HandlerAdapter: "Adjust this method → home(Model model)"
  │       └── model.addAttribute(...)   ← ordinary Java method call (reflection)
  │       └── return "home"
  ├── ViewResolver: "home" → /WEB-INF/views/home.jsp
  └── rendering JSP, write resp

Let’s compare it to:

Tomcat = The front desk of the building (only knows one entrance))
DispatcherServlet = The front desk guy (the only one) Servlet)
@RequestMapping = Company internal work station number(Spring For my own use, Tomcat have no idea)

so @RequestMapping Is Spring MVC in DispatcherServlet This "super Servlet" does it internallySecondary routingmark. For Tomcat, there is only one DispatcherServlet Dealing with it.

Q8: Does MySpringMvcInitializer do the same thing as HelloServletAppInitializer and ApiServletAppInitializer?

They are indeed the same system at the SPI discovery mechanism level, but they do things at completely different levels.

All three will eventually be ServletContainerInitializer pass @HandlesTypes Discover and call onStartup(ServletContext). The processes are:

Container startup
  → scanning META-INF/services/javax.servlet.ServletContainerInitializer
    → @HandlesTypes(WebApplicationInitializer.class)
      → Find all implementation classes → call onStartup()

But the key difference is:

DimensionsMySpringMvcInitializerHelloServlet/ApiServlet Initializer
inheritance chainWebApplicationInitializerAbstractContextLoaderInitializerAbstractDispatcherServletInitializerAbstractAnnotationConfigDispatcherServletInitializerDirect implementation AppInitializer(equivalent to WebApplicationInitializer)
things to doStart the entire Spring container (create two ApplicationContexts Root + Servlet, register DispatcherServlet)Directly register native Servlet + Filter
Request processing methodTake Spring MVC's DispatcherServlet → HandlerMapping → HandlerAdapter → @Controller complete linkDirectly by the Servlet service() / doGet() Processed without Spring involvement
Configuration methodpass getRootConfigClasses() / getServletConfigClasses() return @Configuration kinddirectly in onStartup() Chinese use addServlet() / addFilter() Programmatic registration

Simple analogy:

// HelloServletAppInitializer: Handmade workshop, register the original by yourself Servlet
ctx.addServlet("hello", new HelloServlet());  // direct Servlet, Not leaving Spring

// MySpringMvcInitializer: A factory was built and the entire Spring MVC pull up
getRootConfigClasses();     // → create Root ApplicationContext(Service/DAO layer)
getServletConfigClasses();  // → create Servlet ApplicationContext(Controller layer)
// The final registration was DispatcherServlet, It uniformly distributes requests to @Controller

But be careful about the accuracy of the word "start": Spring MVC does not "start first and then load MySpringMvcInitializer". The actual order of occurrence is:

① Tomcat Container startup
   ↓
② Tomcat scanning SpringServletContainerInitializer(Spring provided SPI Entrance)
   ↓
③ SpringServletContainerInitializer pass @HandlesTypes Discover MySpringMvcInitializer
   ↓
④ reflection creation MySpringMvcInitializer instance, call onStartup()
   ↓
⑤ onStartup() Created internally Spring container: 
     → create Root ApplicationContext(Service/DAO layer)
     → create Servlet ApplicationContext(Controller layer)
     → register DispatcherServlet arrive Tomcat

At this time Spring MVC was born for the first time. To put it bluntly:MySpringMvcInitializer is the "igniter" and Tomcat is the "lighter"——Tomcat turns the lighter first (triggering SPI), but the spark really ignites the Spring MVC engine. MySpringMvcInitializer.onStartup().

Teaching instructions: Sample code of this project (HelloServletAppInitializer, ApiServletAppInitializer, ListenerDemoAppInitializer) split into multiple independent AppInitializer Implementation classes - each class focuses on demonstrating a registration method (Servlet, Filter, Listener), making it easier for readers to understand separately. In actual projects, it will not be split like this, usually one MySpringMvcInitializer Includes all registration logic (Servlet + Filter + Listener), or directly use Spring Boot to automatically configure everything.


1.1 Detailed explanation of Servlet registration API

Theoretical explanation:

ServletRegistration.Dynamic It is a programmatic registration interface introduced by Servlet 3.0 and returns Dynamic Objects are allowed ininitialization phase(Container calls onStartup() period) dynamically configures the servlet's metadata, thereby completely replacing web.xml in <servlet> and <servlet-mapping> statement.

Responsibilities of core methods:

methodeffectAnalogy web.xml elementCalling time
addServlet(name, servlet)Register a Servlet instance with the container and return the registration handle<servlet><servlet-name>Must be called during container initialization phase (within onStartup)
addMapping(patterns)Specify URL path mapping rules<servlet-mapping><url-pattern>Configure immediately after registration
setLoadOnStartup(int)Set boot load priority.>=0 Indicates initialization when the container starts;<0 Or omitted, it means lazy loading on the first request.<load-on-startup>After registration
setAsyncSupported(boolean)Whether to support asynchronous Servlet processing (servlet 3.0 new feature)<async-supported>After registration
setInitParameter(k, v)Set Servlet initialization parameters, which can be found in getInitParameter() Read in<init-param>After registration

Key constraints:

  • addServlet() Can only be called once(unique for each name), repeated calls will return null
  • All configuration must be called in the container onStartup() returnBeforecompleted, after Dynamic Object becomes read-only
  • setLoadOnStartup(1) The smaller the value, the higher the priority, and the negative value indicates lazy loading.

Practical example - registration of DispatcherServlet in Spring:

// Source code source: AbstractDispatcherServletInitializer.registerDispatcherServlet()
// This is Spring MVC frame internal pair Servlet register API actual use of

protected void registerDispatcherServlet(ServletContext servletContext) {
    String servletName = getServletName();                  // → "dispatcher"
    WebApplicationContext servletAppContext = createServletApplicationContext();
    FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);

    // ★ Core: Programmatically API register DispatcherServlet
    ServletRegistration.Dynamic registration =
            servletContext.addServlet(servletName, dispatcherServlet);

    // Configuration Servlet Behavior
    registration.setLoadOnStartup(1);                       // Initialized immediately when the container starts
    registration.addMapping(getServletMappings());          // → "/" Default mapping
    registration.setAsyncSupported(isAsyncSupported());     // → true Asynchronous support

    // Optional custom configuration (hook method, subclasses can override)
    customizeRegistration(registration);
}

Practical example - customized business scenario (multiple Servlet isolation):

// Scene: one Web The application provides both page browsing and REST API Serve, 
// use two different Servlet quarantine

public class MultiServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext ctx) {
        // --- Servlet 1: page Servlet ---
        ServletRegistration.Dynamic pageServlet =
                ctx.addServlet("page", new PageServlet());
        pageServlet.addMapping("/page/*");
        pageServlet.setLoadOnStartup(1);

        // --- Servlet 2: API Servlet ---
        ServletRegistration.Dynamic apiServlet =
                ctx.addServlet("api", new ApiServlet());
        apiServlet.addMapping("/api/*");
        apiServlet.setLoadOnStartup(2);          // Lower priority than page Servlet
        apiServlet.setAsyncSupported(true);      // API Support asynchronous

        // --- Servlet 3: health check Servlet(Lazy loading) ---
        ServletRegistration.Dynamic healthServlet =
                ctx.addServlet("health", new HealthServlet());
        healthServlet.addMapping("/health");
        // Not called setLoadOnStartup() → Initialized only on first request
    }
}

1.2 Detailed explanation of Filter registration API

Theoretical explanation:

FilterRegistration.Dynamic Provides the ability to register Filter programmatically. Its core value is:

  1. Precisely control the interception range -- pass addMappingForUrlPatterns() and addMappingForServletNames() two ways
  2. Control execution order -- pass setInitParameter() or EnumSet<DispatcherType> parameter
  3. Complete replacement web.xml in <filter> and <filter-mapping>

Core methods:

methodeffectDescription of key parameters
addFilter(name, filter)Register Filter instancename is unique within the same context
addMappingForUrlPatterns(dispatchers, isMatchAfter, patterns)Match by URL pathdispatchers: Request type enumeration (REQUEST/FORWARD/INCLUDE/ERROR/ASYNC);isMatchAfter: true means appending after existing mapping (lower priority)
addMappingForServletNames(dispatchers, isMatchAfter, names)Match by Servlet nameOnly intercept requests from the specified Servlet
setInitParameter(k, v)Set initialization parametersexist init(FilterConfig) Read in

dispatchers Detailed explanation of parameters (DispatcherType):

DispatcherTypeTrigger timeTypical scenario
REQUESTHTTP request initiated directly by the clientMost business filters (permissions, logs, encoding)
FORWARDrequest.getRequestDispatcher().forward()Prevent Forward from repeatedly logging
INCLUDErequest.getRequestDispatcher().include()JSP <jsp:include> special treatment when
ERROR<error-page> Mechanism triggerSpecial handling of error pages
ASYNCAsynchronous Servlet's startAsync()Context cleanup for asynchronous requests

Practical example - the registration mechanism of Filter in Spring:

// Source code source: AbstractDispatcherServletInitializer.registerServletFilter()
// Spring Will Filter Register to DispatcherServlet The core logic of

private FilterRegistration.Dynamic registerServletFilter(
        ServletContext servletContext, Filter filter) {

    String filterName = filter.getClass().getSimpleName();
    // register Filter
    FilterRegistration.Dynamic registration =
            servletContext.addFilter(filterName, filter);

    // set up Filter intercept all DispatcherServlet Processed requests
    // REQUEST + FORWARD + INCLUDE + ASYNC: Covers all common request types
    registration.addMappingForUrlPatterns(
            EnumSet.of(DispatcherType.REQUEST,
                       DispatcherType.FORWARD,
                       DispatcherType.INCLUDE,
                       DispatcherType.ASYNC),
            false,                          // false = Add to the front of the mapping list (high priority)
            getServletMappings());          // → and DispatcherServlet The mapping paths are consistent
    return registration;
}

Practical example - enterprise-level Filter chain orchestration:

// Scene: multiple Filter Registration order and execution order control

public class FilterChainInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext ctx) {
        // ★ Registration order = Execution order (for the same mapping Way)

        // No.1registration: character encoding Filter(executed first)
        FilterRegistration.Dynamic encodingFilter =
                ctx.addFilter("encoding", new CharacterEncodingFilter("UTF-8"));
        encodingFilter.addMappingForUrlPatterns(
                EnumSet.of(DispatcherType.REQUEST), true, "/*");

        // No.2Registration: Security Certification Filter(second execution)
        FilterRegistration.Dynamic securityFilter =
                ctx.addFilter("security", new SecurityFilter());
        securityFilter.addMappingForUrlPatterns(
                EnumSet.of(DispatcherType.REQUEST), true, "/admin/*");

        // No.3Registrations: Request Log Filter(third execution)
        FilterRegistration.Dynamic logFilter =
                ctx.addFilter("logging", new LoggingFilter());
        logFilter.addMappingForUrlPatterns(
                EnumSet.of(DispatcherType.REQUEST), true, "/*");

        // pass Servlet Name exact match (interception only "api" Servlet request)
        FilterRegistration.Dynamic apiAuthFilter =
                ctx.addFilter("apiAuth", new ApiAuthFilter());
        apiAuthFilter.addMappingForServletNames(
                EnumSet.of(DispatcherType.REQUEST), true, "api");
    }
}

1.3 Detailed explanation of Listener registration API

Theoretical explanation:

ServletContext.addListener() It is the simplest of the three APIs - you only need to pass in the Listener instance, and the container will automatically Implemented interface typeto determine its use. The Servlet 3.0 specification defines 8 core Listener interfaces, which are divided into three categories according to the listening objects:

Category 1: ServletContext life cycle (application level)

Listener interfaceTrigger timeTypical uses
ServletContextListenercontextInitialized() / contextDestroyed()Application start/stopInitialize/destroy global resources (DataSource, thread pool) when
ServletContextAttributeListenerWhen adding/deleting/modifying attributesMonitor changes to global context properties

Category 2: HttpSession life cycle (session level)

Listener interfaceTrigger timeTypical uses
HttpSessionListenersessionCreated() / sessionDestroyed()Online user statistics,Session timeout cleaning
HttpSessionAttributeListenerWhen adding/deleting/modifying session attributesMonitor data changes in user sessions
HttpSessionActivationListenerDuring passivation/activationSession migration processing in distributed environment
HttpSessionBindingListenerWhen binding/unbinding objectsNotify that the object itself is put into/out of the Session

Category 3: ServletRequest life cycle (request level)

Listener interfaceTrigger timeTypical uses
ServletRequestListenerrequestInitialized() / requestDestroyed()Request time-consuming statistics, MDC log context injection
ServletRequestAttributeListenerWhen requesting attribute addition/deletion/modificationAudit trail of request parameters

Practical example - the use of ContextLoaderListener in Spring:

// Source code source: AbstractContextLoaderInitializer.registerContextLoaderListener()
// Spring use ServletContextListener to manage Root WebApplicationContext life cycle

private void registerContextLoaderListener(ServletContext servletContext) {
    WebApplicationContext rootContext = createRootApplicationContext();
    if (rootContext != null) {
        // register ContextLoaderListener → it achieved ServletContextListener
        // contextInitialized() → create Root WebApplicationContext
        // contextDestroyed()  → closure Root WebApplicationContext
        servletContext.addListener(new ContextLoaderListener(rootContext));
    }
}

// ContextLoaderListener The internal simplified logic of: 
public class ContextLoaderListener implements ServletContextListener {

    private WebApplicationContext context;

    public ContextLoaderListener(WebApplicationContext context) {
        this.context = context;
    }

    @Override
    public void contextInitialized(ServletContextEvent event) {
        // ★ When the application starts: initialization Root container
        // execute all Bean Instantiation, dependency injection, initialization methods
        ((ConfigurableWebApplicationContext) context).refresh();
        event.getServletContext().log("Root WebApplicationContext Initialization completed");
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        // ★ When the app is closed: gracefully close Root container
        // execute all @PreDestroy Callback, release connection pool, close thread pool
        ((ConfigurableWebApplicationContext) context).close();
    }
}

Practical example - enterprise-level Listener combination application:

// Scene: a complete Web The application requires initialization at startup, online statistics, and request auditing

public class AppListenersInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext ctx) {

        // 1. Application launch/Close: Initialize the global thread pool + graceful closing
        ctx.addListener(new AppLifecycleListener());

        // 2. Online user statistics: record current activity Session number
        ctx.addListener(new OnlineUserListener());

        // 3. Request auditing: the beginning of each request/End logging
        ctx.addListener(new RequestAuditListener());
    }
}

// ===== Listener 1: Application lifecycle management =====
public class AppLifecycleListener implements ServletContextListener {
    private ScheduledExecutorService scheduler;

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // Initialize the global scheduled task thread pool
        scheduler = Executors.newScheduledThreadPool(4);
        sce.getServletContext().setAttribute("scheduler", scheduler);
        System.out.println("[App] The application starts and the thread pool is initialized");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // Gracefully shut down the thread pool
        if (scheduler != null) {
            scheduler.shutdown();
            System.out.println("[App] The application is closed and the thread pool has stopped gracefully");
        }
    }
}

// ===== Listener 2: Online user statistics =====
public class OnlineUserListener implements HttpSessionListener {
    private int activeSessions = 0;

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        activeSessions++;
        se.getSession().getServletContext()
                .setAttribute("onlineUsers", activeSessions);
        System.out.println("[Session] New session created, currently online: " + activeSessions);
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        activeSessions--;
        se.getSession().getServletContext()
                .setAttribute("onlineUsers", activeSessions);
        System.out.println("[Session] Session destroyed, currently online: " + activeSessions);
    }
}

// ===== Listener 3: Request an audit =====
public class RequestAuditListener implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        // exist MDC Inject the request into ID, Facilitates log tracking
        String requestId = UUID.randomUUID().toString().substring(0, 8);
        MDC.put("reqId", requestId);
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        // clean up MDC context to prevent memory leaks
        MDC.clear();
    }
}

1.4 Comparison and summary of the three major registration APIs

DimensionsServlet registrationFilter registrationListener registration
Registration methodaddServlet(name, instance)addFilter(name, instance)addListener(instance)
Return typeServletRegistration.DynamicFilterRegistration.Dynamicvoid(no return value)
Configuration contentURL mapping, startup sequence, asynchronous support, initialization parametersURL pattern matching, Servlet name matching, request typeNone (the type is determined by the interface)
execution timingRequest processing (doGet/doPost)Request interception (doFilter)Life cycle callbacks (initialization/destruction/creation/destination)
Typical applicationsDispatcherServlet, REST API ServletCoding, security, logging, performance monitoringGlobal resource initialization, online statistics, request audit
Multiple instance relationshipsProcess requests independently without interfering with each otherForm FilterChain and execute sequentiallyEach monitors different events and does not depend on each other.
Spring encapsulationDispatcherServletRegistrationBeanFilterRegistrationBeanContextLoaderListener(ServletContextListener)

Core design ideas:

  • Servlet = request handler (handles "what to do")
  • Filter = request interceptor (handles "what to do before/after")
  • Listener = event listener (handles "what to do when something happens")

These three constitute the Servlet containerInterceptor chain + handler + observerThe complete architecture of Servlet 3.0, and the programmatic registration API of Servlet 3.0 allows the assembly of these components to be completely separated from XML, realizing a modern development model that is type-safe and IDE-friendly.


2. Detailed explanation of SPI convention of Servlet 3.0

The Servlet 3.0 specification defines a special file path for Servlet containers (Tomcat, Jetty, etc.)mustScan and parse it on startup:

META-INF/services/javax.servlet.ServletContainerInitializer

2.1 Core interface

package javax.servlet;

import java.util.Set;

public interface ServletContainerInitializer {
    /**
     * This method is automatically called when the container starts
     * @param c   @HandlesTypes The type collection specified by the annotation (the container automatically collects)
     * @param ctx ServletContext, for programmatic registration Servlet/Filter/Listener
     */
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

@HandlesTypes The role of annotations:

@HandlesTypes(WebApplicationInitializer.class)
class MyInitializer implements ServletContainerInitializer {
    /**
     * The container will scan classpath Download all WebApplicationInitializer subtype of
     * collect them Set<Class<?>> c incoming
     * Note: The container may also stuff interfaces and abstract classes into the collection, which needs to be filtered by itself.
     */
}

2.2 SPI file loading process (container internal perspective)

Tomcat start up
  │
  ├── Scan all jar Bag → Find META-INF/services/javax.servlet.ServletContainerInitializer
  │
  ├── Parse file content → Get a list of fully qualified class names
  │
  ├── Execute for each class name: 
  │     ├── Reflection loads this class
  │     ├── Check if marked @HandlesTypes
  │     │     └── have → scanning classpath Collect all matching subtypes
  │     │     └── none → c The set is null
  │     └── Instantiate → call onStartup(c, servletContext)
  │
  └── SpringServletContainerInitializer.onStartup() called
        → Get all WebApplicationInitializer accomplish
        → sort → Instantiate one by one → implement onStartup()

2.3 Complete SPI demo code

package com.example.servlet3demo;

import javax.servlet.*;
import javax.servlet.annotation.HandlesTypes;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;

// ============================================================
// Mark interface: Any class that implements this interface will be automatically discovered when the container starts.
// ============================================================
interface AppInitializer {
    void onStartup(ServletContext ctx);
}

// ============================================================
// Specific initialization logic: Register one programmatically Servlet
// ============================================================
class MyWebAppInitializer implements AppInitializer {
    @Override
    public void onStartup(ServletContext ctx) {
        ServletRegistration.Dynamic reg = ctx.addServlet("hello", new HelloServlet());
        reg.addMapping("/hello");
        reg.setLoadOnStartup(1);
        System.out.println("[INFO] HelloServlet Registered programmatically");

        // You can also register Filter
        FilterRegistration.Dynamic filterReg = ctx.addFilter("logging", new LoggingFilter());
        filterReg.addMappingForUrlPatterns(null, true, "/*");
    }
}

// ============================================================
// a simple HttpServlet
// ============================================================
class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println("<h1>Servlet 3.0 Self-registration successful!</h1>");
        out.println("<p>this Servlet Not here web.xml Statement in</p>");
        out.println("<p>pass SPI document + ServletContainerInitializer Automatically discover and register</p>");
    }
}

// ============================================================
// Filter accomplish
// ============================================================
class LoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        long start = System.currentTimeMillis();
        chain.doFilter(req, resp);
        long elapsed = System.currentTimeMillis() - start;
        System.out.println("[LOG] " + req.getRemoteAddr() + " - time consuming: " + elapsed + "ms");
    }
    @Override public void init(FilterConfig config) {}
    @Override public void destroy() {}
}

// ============================================================
// SPI Entry class (matching @HandlesTypes)
// Need to be in META-INF/services/javax.servlet.ServletContainerInitializer Statement in
// ============================================================
@HandlesTypes(AppInitializer.class)
class MyServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> initializerClasses, ServletContext ctx)
            throws ServletException {
        if (initializerClasses == null || initializerClasses.isEmpty()) {
            ctx.log("[WARN] not found any AppInitializer Implementation class");
            return;
        }

        for (Class<?> clazz : initializerClasses) {
            // Key defensive checks: filtering interfaces, abstract classes, non-target types
            if (clazz.isInterface()
                    || java.lang.reflect.Modifier.isAbstract(clazz.getModifiers())
                    || !AppInitializer.class.isAssignableFrom(clazz)) {
                continue;
            }
            try {
                AppInitializer initializer = (AppInitializer)
                        clazz.getDeclaredConstructor().newInstance();
                initializer.onStartup(ctx);
                ctx.log("[INFO] " + clazz.getName() + " Initialization completed");
            } catch (Exception e) {
                throw new ServletException("cannot be instantiated " + clazz.getName(), e);
            }
        }
    }
}

SPI configuration file location: src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer

File content (one line):

com.example.servlet3demo.MyServletContainerInitializer

3. ServletContainerInitializer implementation of Spring framework

Spring is in spring-web The module has its own SPI entry built into it. Open the Spring jar package and you can see:

spring-web-xxx.jar/META-INF/services/javax.servlet.ServletContainerInitializer:

org.springframework.web.SpringServletContainerInitializer

3.1 SpringServletContainerInitializer source code analysis

// Source code location: org.springframework.web.SpringServletContainerInitializer
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses,
                          ServletContext servletContext) throws ServletException {

        // No. 1 Step: Collect all non-interface, non-abstract, instantiable classes
        List<WebApplicationInitializer> initializers = new LinkedList<>();
        if (webAppInitializerClasses != null) {
            for (Class<?> waiClass : webAppInitializerClasses) {
                if (!waiClass.isInterface()
                        && !Modifier.isAbstract(waiClass.getModifiers())
                        && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)
                                waiClass.getDeclaredConstructor().newInstance());
                    } catch (Throwable ex) {
                        throw new ServletException(
                                "Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }

        // No. 2 Step: Empty collection processing
        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }

        servletContext.log(initializers.size()
                + " Spring WebApplicationInitializers detected on classpath");

        // No. 3 Step: press @Order Execute in sequence after sorting
        AnnotationAwareOrderComparator.sort(initializers);
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }
}

Key design points:

  • use @HandlesTypes(WebApplicationInitializer.class) Let the container automatically collect all implementation classes
  • Filter interfaces and abstract classes (defensive programming, some Servlet containers are not standardized)
  • use AnnotationAwareOrderComparator sorting, support @Order and Ordered interface

3.2 WebApplicationInitializer interface

// Source code location: org.springframework.web.WebApplicationInitializer
public interface WebApplicationInitializer {
    /**
     * exist Servlet The container is started by SpringServletContainerInitializer callback
     * Developers register programmatically in this method Servlet, Filter, Listener
     */
    void onStartup(ServletContext servletContext) throws ServletException;
}

4. In-depth analysis of inheritance chain

In order to reduce the difficulty of development, Spring provides three levels of abstract base classes to form a clear chain of responsibility:

WebApplicationInitializer (interface)
  │  onStartup(ServletContext)
  │
  ├── AbstractContextLoaderInitializer (abstract class)
  │    Responsibilities: create Root WebApplicationContext(Service/DAO layer)
  │    key methods: createRootApplicationContext()
  │
  ├── AbstractDispatcherServletInitializer (abstract class)
  │    Responsibilities: create Servlet WebApplicationContext(Controller layer)
  │          register DispatcherServlet
  │    key methods:
  │      - createServletApplicationContext()
  │      - createDispatcherServlet()
  │      - getServletMappings()     ← Subclasses must implement
  │      - getServletName()         ← Subclasses must implement
  │      - isAsyncSupported()       ← Overwriteable, default true
  │
  └── AbstractAnnotationConfigDispatcherServletInitializer (abstract class)
       Responsibilities: Based on annotation configuration, convention is better than configuration
       key methods:
        - getRootConfigClasses()
        - getServletConfigClasses()

┌─ your implementation class ──────────────────────┐
│ extends AbstractAnnotationConfigDispatcherServletInitializer │
│ Just implement 3-4 Just an abstract method        │
└────────────────────────────────────┘

4.1 Layer-by-layer source code analysis

// ============================================================
// first floor: Root ApplicationContext initialization
// Source code: org.springframework.web.context.AbstractContextLoaderInitializer
// ============================================================
abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // create Root WebApplicationContext(usually contains DataSource, Service, DAO)
        registerContextLoaderListener(servletContext);
    }

    private void registerContextLoaderListener(ServletContext servletContext) {
        WebApplicationContext rootContext = createRootApplicationContext();
        if (rootContext != null) {
            // register ContextLoaderListener, managed by it Root Context life cycle
            servletContext.addListener(
                    new ContextLoaderListener(rootContext));
        } else {
            // rootContext for null Indicates no need Root Context(If only Controller No Service)
            logger.debug("No root context configured");
        }
    }

    // Subclasses can override this method to provide Root Context
    @Nullable
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }
}

// ============================================================
// second floor: DispatcherServlet register
// Source code: org.springframework.web.servlet.support.AbstractDispatcherServletInitializer
// ============================================================
abstract class AbstractDispatcherServletInitializer
        extends AbstractContextLoaderInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // Execute parent class logic first → initialization Root Context
        super.onStartup(servletContext);

        // Register again DispatcherServlet
        registerDispatcherServlet(servletContext);
    }

    // ==================== Registration process (template method pattern) ====================
    protected void registerDispatcherServlet(ServletContext servletContext) {
        String servletName = getServletName();
        WebApplicationContext servletAppContext = createServletApplicationContext();

        // create DispatcherServlet Instance (can be overridden by subclasses))
        FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);

        // Programmatically register to Servlet container
        ServletRegistration.Dynamic registration =
                servletContext.addServlet(servletName, dispatcherServlet);
        registration.setLoadOnStartup(1);
        registration.addMapping(getServletMappings());
        registration.setAsyncSupported(isAsyncSupported());

        // Register for additional Filter(if there is)
        Filter[] filters = getServletFilters();
        if (filters != null && filters.length > 0) {
            for (Filter filter : filters) {
                registerServletFilter(servletContext, filter);
            }
        }
    }

    // ==================== Overridable hook methods ====================
    protected FrameworkServlet createDispatcherServlet(
            WebApplicationContext servletAppContext) {
        return new DispatcherServlet(servletAppContext);
    }

    protected boolean isAsyncSupported() {
        return true;
    }

    @Nullable
    protected Filter[] getServletFilters() {
        return null;
    }

    // ==================== Abstract methods that subclasses must implement ====================
    protected abstract String getServletName();
    protected abstract String[] getServletMappings();
    protected abstract WebApplicationContext createServletApplicationContext();
}

// ============================================================
// The third layer: annotation configuration support
// Source code: org.springframework.web.servlet.support
//       .AbstractAnnotationConfigDispatcherServletInitializer
// ============================================================
abstract class AbstractAnnotationConfigDispatcherServletInitializer
        extends AbstractDispatcherServletInitializer {

    @Override
    protected WebApplicationContext createServletApplicationContext() {
        // create AnnotationConfigWebApplicationContext(annotation driven ApplicationContext)
        AnnotationConfigWebApplicationContext context =
                new AnnotationConfigWebApplicationContext();

        // register @Configuration kind
        Class<?>[] configClasses = getServletConfigClasses();
        if (configClasses != null && configClasses.length > 0) {
            context.register(configClasses);
        }
        return context;
    }

    @Override
    @Nullable
    protected WebApplicationContext createRootApplicationContext() {
        Class<?>[] configClasses = getRootConfigClasses();
        if (configClasses == null || configClasses.length == 0) {
            return null;  // No Root Config then skip
        }
        AnnotationConfigWebApplicationContext rootContext =
                new AnnotationConfigWebApplicationContext();
        rootContext.register(configClasses);
        return rootContext;
    }

    // Subclasses can provide configuration classes
    @Nullable
    protected abstract Class<?>[] getRootConfigClasses();
    protected abstract Class<?>[] getServletConfigClasses();
}

4.2 Practical usage of subclasses

// ============================================================
// Usage in actual projects - just inherit and implement the abstract method
// ============================================================
public class MyWebApplicationInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };  // Service/DAO Layer configuration
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };   // Controller Layer configuration
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };                 // Default mapping
    }

    @Override
    protected String getServletName() {
        return "dispatcher";
    }

    @Override
    protected boolean isAsyncSupported() {
        return true;                                 // Enable async support
    }
}

4.3 Detailed explanation of the relationship between parent and child containers

Q: Why is it said that the Root ApplicationContext corresponds to the Service/DAO layer and the Servlet ApplicationContext corresponds to the Controller layer? Where is this connection reflected in the source code?

When reading the source code in Section 4.1, you will find that the codes of the three abstract classes do not have hard-coded correspondences such as "Root = Service/DAO" and "Servlet = Controller". This relationship is actually determined by two levels:

Level 1: Architectural design of parent-child containers

Spring MVC adoptionDual container (Parent-Child) model, there is a parent-child relationship between the two ApplicationContexts:

Root ApplicationContext (parent container)
  ├── DataSource
  ├── UserService (@Service)
  └── UserDao (@Repository)
       ↑
       │ subcontainer passes setParent() Associated parent container
       │ Find Bean Time: Search yourself first → If not found, search the parent container upwards.
       │
Servlet ApplicationContext (subcontainer)
  ├── UserController (@Controller)
  ├── ViewResolver
  └── HandlerMapping

Key mechanisms:

  • Child containers can access the beans of the parent container -- so @Controller middle @Autowired UserService can be injected successfully
  • The parent container cannot access the beans of the child container ——So the Service layer will not rely on the Web layer in reverse, ensuring a clean layering
  • When there are multiple DispatcherServlets, they share the same Root Context to avoid Service/DAO from being loaded repeatedly.

Level 2: Convention is better than configuration

The source code itself does not limit which container contains which Bean. The corresponding relationship is provided by you. @Configuration class to create:

// getRootConfigClasses() → The returned configuration class was scanned Service/DAO Bag
@Override
protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { RootConfig.class };
    // RootConfig middle: 
    //   @ComponentScan("com.example.service")
    //   @ComponentScan("com.example.dao")
    //   → These were scanned Bean Just entered Root Context
}

// getServletConfigClasses() → The returned configuration class was scanned Controller Bag
@Override
protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { WebConfig.class };
    // WebConfig middle: 
    //   @ComponentScan("com.example.controller")
    //   @EnableWebMvc  // register HandlerMapping, ViewResolver wait MVC infrastructure
    //   → These were scanned Bean Just entered Servlet Context
}

That is to say:The reason why Root Context is "equal" to the Service/DAO layer is because you are getRootConfigClasses() The Service/DAO package was scanned in the configuration class; The reason why Servlet Context is "equal" to the Controller layer is for the same reason. This is a layering practice officially recommended by Spring and is not a mandatory constraint of the framework code.

Why split into two containers?

benefitillustrate
Hierarchical isolationThe Service layer does not know the existence of the Web layer and cannot rely on the Controller in reverse - the architecture is cleaner
Multiple Servlet sharingA Root Context can be shared by multiple Servlet Contexts (for example, admin-dispatcher + api-dispatcher share the same set of Service/DAO) to avoid repeated loading of beans.
Transaction boundaries are correctThe transaction manager is placed in the Root Context. All sub-containers share the same transaction manager. The AOP transaction aspect can correctly wrap Service method calls.
Easy to testThe Root Context test Service layer can be loaded separately without starting the Web layer.

See details 6. Analysis of key design patterns → 6.2 Parent-child ApplicationContext design.


5. Complete timing diagram

  Servletcontainer           SpringServletContainer         AbstractContext          AbstractDispatcher
  (Tomcat)              Initializer                    LoaderInitializer        ServletInitializer
     │                        │                             │                        │
     │  ① scanning META-INF/      │                             │                        │
     │  services/ document        │                             │                        │
     │────────────────────────▶                             │                        │
     │                        │                             │                        │
     │  ② Instantiate and call        │                             │                        │
     │  onStartup()           │                             │                        │
     │────────────────────────▶                             │                        │
     │                        │                             │                        │
     │  ③ @HandlesTypes       │                             │                        │
     │  collect WebAppInit       │                             │                        │
     │  Implement classes and filter and sort      │                             │                        │
     │                        │                             │                        │
     │                        │  ④ Call one by one onStartup()     │                        │
     │                        │─────────────────────────────▶                        │
     │                        │                             │                        │
     │                        │                             │  ⑤ super.onStartup()  │
     │                        │                             │────────────────────────▶
     │                        │                             │                        │
     │                        │                             │  ⑥ registerDispatcher │
     │                        │                             │  Servlet()             │
     │                        │                             │◀────────────────────────
     │                        │                             │                        │
     │                        │                             │  ⑦ servletContext.     │
     │                        │                             │  addServlet() ★        │
     │                        │                             │────────► Container registration      │
     │                        │                             │                        │
     │  ★ Newly registered Servlet/   │                             │                        │
     │  Filter Take effect           │                             │                        │
     │◀─────────────────────────────────────────────────────│                        │

Timing diagram description:

stepillustratekey code
Tomcat scans SPI files in jar packagesMETA-INF/services/javax.servlet...
Instantiate Spring entry classSpringServletContainerInitializer
collect WebApplicationInitializer All subtypes@HandlesTypes(WebApplicationInitializer.class)
Sorting, instantiation, calling one by oneAnnotationAwareOrderComparator.sort()
First call the parent class to initialize the Root Contextsuper.onStartup(servletContext)
Create Servlet Context + Register DispatcherServletregisterDispatcherServlet()
Programmatic injection into Servlet containerservletContext.addServlet(name, servlet)

6. Analysis of key design patterns

6.1 Template method pattern

AbstractContextLoaderInitializer.onStartup()
  └── createRootApplicationContext()        ← Abstract method, subclass implementation

AbstractDispatcherServletInitializer.onStartup()
  └── super.onStartup()                     ← Call parent class
  └── registerDispatcherServlet()           ← Define algorithm skeleton
        ├── getServletName()                ← Abstract method, subclass implementation
        ├── createServletApplicationContext() ← Abstract method, subclass implementation
        ├── createDispatcherServlet()       ← Overridable, created by default DispatcherServlet
        ├── getServletMappings()            ← Abstract method, subclass implementation
        └── getServletFilters()             ← Overwriteable, default null

Why use template method pattern?

  • Encapsulate the fixed registration process in the base class, and the subclass only needs to provide changing data
  • Comply with the "opening and closing principle" - new functions only need to extend the subclass, no need to modify the base class
  • Provide hook methods (such as createDispatcherServlet()) Customization for advanced users

6.2 Parent-child ApplicationContext design

// Root ApplicationContext ─── global sharing Bean
//   ├── DataSource
//   ├── @Service  Bean
//   └── @Repository Bean
//
// Servlet ApplicationContext ─── Web layer-specific Bean
//   ├── @Controller Bean
//   ├── ViewResolver
//   └── HandlerMapping
//
// relation: 
//   Servlet Context Can access Root Context of Bean
//   Root Context Cannot access Servlet Context of Bean
//   └── This is why the transaction manager is Root Context Medium configuration
//       and Controller exist Servlet Context Medium configuration

6.3 Defensive Programming

SpringServletContainerInitializer Triple filtering in:

// Why filter interfaces and abstract classes?
// in some old Servlet Container is being implemented, @HandlesTypes Annotations may
// The interface itself and abstract classes are also added Set<Class<?>> middle.
// Without filtering, directly clazz.newInstance() will throw InstantiationException
if (!waiClass.isInterface()                       // exclude interface
        && !Modifier.isAbstract(waiClass.getModifiers())  // exclude abstract classes
        && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { // type checking
    initializers.add(...);
}

7. Practical application scenarios and extensions

Scenario 1: Customizing DispatcherServlet behavior

public class CustomDispatcherServletInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/api/*" };
    }

    @Override
    protected String getServletName() {
        return "api-dispatcher";
    }

    // 🔑 Override creation method, enable DispatcherServlet additional features
    @Override
    protected FrameworkServlet createDispatcherServlet(
            WebApplicationContext servletAppContext) {
        DispatcherServlet servlet = new DispatcherServlet(servletAppContext);
        // not found Handler throws an exception (instead of returning 404), Facilitates unified exception handling
        servlet.setThrowExceptionIfNoHandlerFound(true);
        // deal with OPTIONS ask
        servlet.setDispatchOptionsRequest(true);
        // deal with TRACE ask
        servlet.setDispatchTraceRequest(false);
        return servlet;
    }

    @Override
    protected boolean isAsyncSupported() {
        return false;  // Turn off asynchronous (simple scenarios improve performance)
    }
}

Scenario 2: Register additional Filter

public class FilteredDispatcherServletInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

    @Override
    protected String getServletName() {
        return "dispatcher";
    }

    // 🔑 Register custom Filter(will be automatically mapped to DispatcherServlet superior)
    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {
            new CharacterEncodingFilter("UTF-8"),
            new RequestTimingFilter()
        };
    }
}

// character encoding Filter
public class CharacterEncodingFilter implements Filter {
    private final String encoding;
    public CharacterEncodingFilter(String encoding) {
        this.encoding = encoding;
    }
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        req.setCharacterEncoding(encoding);
        resp.setCharacterEncoding(encoding);
        resp.setContentType("text/html;charset=" + encoding);
        chain.doFilter(req, resp);
    }
    @Override public void init(FilterConfig config) {}
    @Override public void destroy() {}
}

// Request time-consuming statistics Filter
public class RequestTimingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        long start = System.nanoTime();  // Nanosecond accuracy
        try {
            chain.doFilter(req, resp);
        } finally {
            long elapsed = System.nanoTime() - start;
            System.out.printf("[TIMING] ask %s time consuming: %.2fms%n",
                    req.getRemoteAddr(), elapsed / 1_000_000.0);
        }
    }
    @Override public void init(FilterConfig config) {}
    @Override public void destroy() {}
}

Scenario 3: Multiple DispatcherServlets (multiple context isolation)

Is there only one DispatcherServlet by default? Yes,AbstractAnnotationConfigDispatcherServletInitializer exist registerDispatcherServlet() Only registered inone DispatcherServlet.In 99% of cases this is enough.——All requests / All in the same one DispatcherServlet, by different @Controller distribution.

But in some scenarios, multiple DispatcherServlets are indeed needed to isolate different business modules:

// Register multiple DispatcherServlet, handle different URL path
// each DispatcherServlet Have its own independent Servlet ApplicationContext
// they share the same Root ApplicationContext(Service/DAO layer)

// 1. Management background
public class AdminServletInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };  // shared Service/DAO
    }
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { AdminWebConfig.class };
    }
    @Override
    protected String[] getServletMappings() {
        return new String[] { "/admin/*" };
    }
    @Override
    protected String getServletName() {
        return "admin-dispatcher";
    }
}

// 2. API interface
public class ApiServletInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };  // shared Service/DAO
    }
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { ApiWebConfig.class };
    }
    @Override
    protected String[] getServletMappings() {
        return new String[] { "/api/*" };
    }
    @Override
    protected String getServletName() {
        return "api-dispatcher";
    }
}

The container structure at this time:

Root ApplicationContext (Service/DAO, shared)
├── DispatcherServlet "admin-dispatcher"ServletContext 1 (Admin Controller)
│   mapping: /admin/*
└── DispatcherServlet "api-dispatcher"ServletContext 2 (API Controller)
    mapping: /api/*

Notice: Multiple DispatcherServlets will bring additional complexity - each ServletContext is independent, exception handling is independent, and interceptors need to be configured separately. In actual projects, a more common approach is to passSingle DispatcherServlet + modular @Controller subcontractingto organize the code instead of actually creating multiple DispatcherServlets. Multiple DispatcherServlets are mainly used forExtreme isolation scene(For example, different teams maintain completely independent web modules, require completely different Spring MVC configurations, etc.) In most cases, a microservice architecture will be a better choice.

Q: Since 99% of scenarios only have one DispatcherServlet, are the only things we can customize that are path matching, Filter, and Listener?

Not entirely true. Path matching, Filter, Listener are just Servlet registration level(Right now AbstractAnnotationConfigDispatcherServletInitializer Several methods that can be overridden). DispatcherServlet also has a complete set ofPluggable Strategy Pattern Architecture, which can be customized far more than that.

Customized hierarchical splitting:

first floor: Servlet Registration layer (this document focuses on)
  → getServletMappings()        ← URL path matching
  → getServletFilters()         ← Filter Registration (such as coding, authentication Filter)
  → createDispatcherServlet()   ← DispatcherServlet behavioral switch
       (throwExceptionIfNoHandlerFound / dispatchOptionsRequest wait)
  → isAsyncSupported()          ← Async support switch

second floor: Spring MVC Internal component layer (in @Configuration passed in class WebMvcConfigurer Configuration)
  → HandlerMapping              ← URL → Controller Method mapping strategy
  → HandlerAdapter              ← How to call different types of handlers
  → HandlerInterceptor          ← prefix/Post interception (in Spring within the system, than Filter more flexible)
  → HandlerExceptionResolver    ← Controller How exceptions are converted to HTTP response
  → ViewResolver                ← Logical view name → actual view(JSP / Thymeleaf / pure JSON)
  → HttpMessageConverter        ← Request body/Response body serialization(JSON ↔ Java object)
  → LocaleResolver              ← internationalization
  → MultipartResolver           ← File upload
  → CorsRegistry                ← Cross-domain configuration
  → etc.……

The relationship between the two layers is:The first layer determines "how DispatcherServlet is registered into Tomcat", and the second layer determines "how DispatcherServlet handles requests internally". In daily development, what we deal with frequently is the second layer - such as adding interceptors for login verification, configuring Jackson serialization strategies, etc.

As for the underlying work such as Socket connection management, HTTP message parsing, and encoding format conversion, these are performed by Tomcat (Servlet container) The request has been processed before it reaches DispatcherServlet - what DispatcherServlet gets is already encapsulated HttpServletRequest / HttpServletResponse Objects, the underlying details are completely transparent to developers.

Scenario 4: Custom sorting control

// When there are multiple WebApplicationInitializer when, you can pass @Order Control execution order
@Order(2)  // The smaller the value, the earlier it is executed.
public class PriorityInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    // ... (Higher priority, register first)
}

@Order(Ordered.LOWEST_PRECEDENCE)
public class FallbackInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    // ... (lowest priority, last registered)
}

// Not specified @Order , the default order is Ordered.LOWEST_PRECEDENCE

8. Best practices and pitfall avoidance guides

questionillustratesolution
Duplicate registrationUse at the same time web.xml and WebApplicationInitializerChoose one, pure Java method is recommended
/ and /* Confuse/ Is the default Servlet mapping;/* Will intercept JSPuse /, for static resources DefaultServletHttpRequestHandler
Use the teaching demo as a production templateThe tutorial code is split into multiple Initializer classes for clarity (e.g. HelloServletAppInitializer, ApiServletAppInitializer Each registers a different Servlet)All registration logic in actual projects should be merged inone Initializer, or use Spring Boot automatic configuration instead
Multiple DispatcherServlet abuseUnnecessary multiple DispatcherServlets lead to duplicate configuration and scattered exception handlingIn 99% of scenarios, one DispatcherServlet is enough, and the code can be organized through modular @Controller subcontracting
Async support mismatchDispatcherServlet enables asynchronous but Filter does not support itMake sure all Filters also call setAsyncSupported(true)
Root Context scopeRoot Context beans cannot be overwritten by Servlet ContextRoot is placed in the global bean (Service/DAO), Servlet is placed in the Web Bean (Controller)
Startup performance@HandlesTypes triggers full classpath scanUsing Spring Boot's auto-configuration can significantly reduce the scope of scanning
Exception causes startup failureAn Initializer's onStartup() throws an exceptionDo it in every initializer try-catch and log
Maven packaging missing SPI filesResource plugin not included META-INF/services/confirm pom.xml of resources The configuration contains this directory

The difference between actual projects vs teaching demos

The Demo project in this tutorial deliberately splits different knowledge points into independent classes (HelloServletAppInitializer, ApiServletAppInitializer, ListenerDemoAppInitializer), this is forteaching clarity——Each class focuses on demonstrating one knowledge point. In actual projects:

Traditional Spring MVC project: All registration logic is merged inone MySpringMvcInitializer middle:

public class MySpringMvcInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override protected Class<?>[] getRootConfigClasses() { return new Class[]{RootConfig.class}; }
    @Override protected Class<?>[] getServletConfigClasses() { return new Class[]{WebConfig.class}; }
    @Override protected String[] getServletMappings() { return new String[]{"/"}; }

    // Filter Also register in this category
    @Override protected Filter[] getServletFilters() {
        return new Filter[]{new CharacterEncodingFilter("UTF-8"), new RequestTimingFilter()};
    }

    // Listener(ContextLoaderListener)Depend on Spring The framework handles it automatically, no manual work required addListener()
}

Spring Boot project: You don’t even need to write the above class.DispatcherServletAutoConfiguration All done automatically. This is completely different from the "one function, one class" approach in the teaching demo - the purpose of teaching is to separateMake it clear, the actual merge is forMake it simple.


9. Corresponding mechanism in Spring Boot

In Spring Boot, developers generally do not need to write WebApplicationInitializer. Spring Boot passedautomatic configurationandEmbedded Servlet ContainerMechanism to fully automate the registration of DispatcherServlet:

9.1 Core configuration class

// Source code location: org.springframework.boot.autoconfigure.web.servlet
//          .DispatcherServletAutoConfiguration

@AutoConfiguration(after = ServletWebServerFactoryAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
public class DispatcherServletAutoConfiguration {

    @Configuration
    @Conditional(DispatcherServletRegistrationCondition.class)
    public static class DispatcherServletRegistrationConfiguration {

        @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
        @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        public DispatcherServletRegistrationBean dispatcherServletRegistration(
                DispatcherServlet dispatcherServlet) {

            DispatcherServletRegistrationBean registration =
                    new DispatcherServletRegistrationBean(dispatcherServlet, "/");
            registration.setName("dispatcherServlet");
            registration.setLoadOnStartup(1);
            return registration;
        }
    }
}

9.2 Comparison with traditional methods

DimensionsTraditional Spring MVCSpring Boot
Configuration methodinherit AbstractAnnotationConfigDispatcherServletInitializeruse application.properties + @SpringBootApplication
containerExternal Tomcat/Jetty, create WAR packageEmbed Tomcat/Jetty/Undertow and create JAR package
DispatcherServlet Createpass createDispatcherServlet() Create manuallyPass automatically DispatcherServletAutoConfiguration create
Custom DispatcherServletoverwrite createDispatcherServlet()accomplish WebMvcRegistrations Interface or injection DispatcherServletRegistrationBean
Servlet GetservletContext.addServlet()DispatcherServletRegistrationBean (ServletContextInitializer)
start triggerExternal container scans SPI filesServletWebServerApplicationContext Automatically triggered on refresh

9.3 Spring Boot customization example

// Method 1: Implementation WebMvcRegistrations Interface (recommended)
@Configuration
public class CustomWebMvcConfig implements WebMvcRegistrations {

    @Override
    public DispatcherServlet getDispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setThrowExceptionIfNoHandlerFound(true);
        return servlet;
    }
}

// Method 2: Injection DispatcherServletRegistrationBean
@Configuration
public class CustomDispatcherConfig {

    @Bean
    public DispatcherServletRegistrationBean dispatcherServletRegistration(
            DispatcherServlet dispatcherServlet) {
        DispatcherServletRegistrationBean registration =
                new DispatcherServletRegistrationBean(dispatcherServlet, "/api/*");
        registration.setName("customDispatcher");
        registration.setLoadOnStartup(1);
        return registration;
    }
}

9.4 Spring Boot 3.x automatic configuration loading mechanism

// Spring Boot 3.x: META-INF/spring/org.springframework.boot.autoconfigure.
//                  AutoConfiguration.imports
// Content (excerpt)):
// org.springframework.boot.autoconfigure.web.servlet.
//   DispatcherServletAutoConfiguration

// Spring Boot 2.x: META-INF/spring.factories
// org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
// org.springframework.boot.autoconfigure.web.servlet.\
//   DispatcherServletAutoConfiguration

10. Jakarta EE migration instructions

projectJava EE 8(Spring 5.x)Jakarta EE 9+(Spring 6.x / Boot 3.x)
package namejavax.servlet.*jakarta.servlet.*
SPI file pathMETA-INF/services/javax.servlet.ServletContainerInitializerMETA-INF/services/jakarta.servlet.ServletContainerInitializer
Servlet containerTomcat 9- / Jetty 9Tomcat 10+ / Jetty 11+
ApplicationContextAnnotationConfigWebApplicationContextAnnotationConfigWebApplicationContext(The package path remains unchanged)

Migration steps (3 minutes to complete):

  1. Global search and replace:javax.servletjakarta.servlet
  2. Ensure SPI file paths are updated simultaneously
  3. Upgrade to Tomcat 10+ or ​​Jetty 11+
  4. Recompile verification

Appendix: Summary of SPI file and container discovery process

┌──────────────────────────────────────────────┐
│       Servlet 3.0+ SPI automatic discovery mechanism           │
├──────────────────────────────────────────────┤
│ 1. Jar package contains:                               │
│    META-INF/services/                          │
│    └── javax.servlet.ServletContainerInitializer│
│    content: org.example.MyInitializer             │
│                                                │
│ 2. container(Tomcat)On startup:                      │
│    ├── Scan all Jar Bag                        │
│    ├── read SPI File content                      │
│    ├── Reflective loading MyInitializer                 │
│    ├── read @HandlesTypes annotation                │
│    ├── scanning classpath collection subtype              │
│    └── call onStartup(classes, ctx)           │
│                                                │
│ 3. exist onStartup() middle:                         │
│    ├── Iterate over a list of subtypes                          │
│    ├── Instantiate non-interface/non-abstract class                  │
│    └── Call one by one onStartup(ServletContext)     │
│                                                │
│ 4. exist WebApplicationInitializer middle:           │
│    ├── create ApplicationContext                │
│    ├── create DispatcherServlet                 │
│    └── servletContext.addServlet() → register     │
└──────────────────────────────────────────────┘

A note on the "simulation" code in the article: This article uses simplified versions of simulation classes in many places (such as SpringServletContainerInitializer copy) to demonstrate the core logic. These codes are instructional versions designed to help understand design ideas rather than replace source code reading. Please view the real Spring source code spring-web Module:

  • org.springframework.web.SpringServletContainerInitializer
  • org.springframework.web.WebApplicationInitializer
  • org.springframework.web.servlet.support.AbstractDispatcherServletInitializer
  • org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer
  • org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration(Boot version)