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.xmlExplicit 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.ServletContainerInitializerSPI 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:
| step | components | what did |
|---|---|---|
| ①Receive | HttpServletRequest | Parse HTTP requests into objects: URL, parameters, Header, Cookie, Body |
| ② Processing | your doGet()/doPost() | Java business logic (query database, call Service, spell results) |
| ③Response | HttpServletResponse | Set status code, header, and write content to the output stream |
| ④Packaging | Servlet 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.
Dimensions Ordinary Servlet DispatcherServlet How to handle requests You are here by yourself doGet()/doPost()Hardly written logicThe request enters → HandlerMapping finds the processor → HandlerAdapter calls @Controllermethod → return responseURL routing @WebServlet("/user"), a Servlet corresponds to a fixed path@RequestMapping("/user/{id}"), supports path variables and RESTful styleParameter binding Manual req.getParameter("name")Automatically bind request parameters to method parameters ( @RequestParam,@PathVariable,@RequestBody)response handling Manual resp.getWriter().print(json)Method returns object → HttpMessageConverterAutomatically convert to JSON/XMLScalability No built-in extension points HandlerMapping / 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 writedoGet()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.
| concept | analogy |
|---|---|
Servlet specification (javax.servlet.*) | USB interface standard (specifies shape, protocol) |
| Tomcat | Specific 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.ServletContextetc.), 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:
| container | Features |
|---|---|
| Tomcat | The most mainstream, lightweight |
| Jetty | Lighter and suitable for embedding |
| Undertow | WildFly default, high performance |
| WebLogic / WebSphere | Enterprise 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:
- Safety: You cannot call
RequestInternal methods of Tomcat, such asrequest.getConnector().stop()Shut down the server - 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 first | Tomcat starts first, then load your WAR | your main() Start first, and then start Tomcat inline |
| Where is the main method? | on Tomcat Bootstrap class | in your own startup class |
| Packing method | war | jar(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:
| Servlet | Mapping path | Registration 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.)
| priority | Matching method | Example | hit request | who wins |
|---|---|---|---|---|
| 1 | exact path | /api/info | only /api/info | none |
| 2 | Path prefix (longest first) | /api/* | /api/xxx, /api/info | ApiServlet |
| 3 | extension | *.jsp | /xxx.jsp | not applicable |
| 4 | DefaultServlet | / | Go back and match all missed requests | DispatcherServlet (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:
| Dimensions | MySpringMvcInitializer | HelloServlet/ApiServlet Initializer |
|---|---|---|
| inheritance chain | WebApplicationInitializer → AbstractContextLoaderInitializer → AbstractDispatcherServletInitializer → AbstractAnnotationConfigDispatcherServletInitializer | Direct implementation AppInitializer(equivalent to WebApplicationInitializer) |
| things to do | Start the entire Spring container (create two ApplicationContexts Root + Servlet, register DispatcherServlet) | Directly register native Servlet + Filter |
| Request processing method | Take Spring MVC's DispatcherServlet → HandlerMapping → HandlerAdapter → @Controller complete link | Directly by the Servlet service() / doGet() Processed without Spring involvement |
| Configuration method | pass getRootConfigClasses() / getServletConfigClasses() return @Configuration kind | directly 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 independentAppInitializerImplementation 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 oneMySpringMvcInitializerIncludes 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:
| method | effect | Analogy web.xml element | Calling 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 returnnull- All configuration must be called in the container
onStartup()returnBeforecompleted, afterDynamicObject 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:
- Precisely control the interception range -- pass
addMappingForUrlPatterns()andaddMappingForServletNames()two ways - Control execution order -- pass
setInitParameter()orEnumSet<DispatcherType>parameter - Complete replacement
web.xmlin<filter>and<filter-mapping>
Core methods:
| method | effect | Description of key parameters |
|---|---|---|
addFilter(name, filter) | Register Filter instance | name is unique within the same context |
addMappingForUrlPatterns(dispatchers, isMatchAfter, patterns) | Match by URL path | dispatchers: Request type enumeration (REQUEST/FORWARD/INCLUDE/ERROR/ASYNC);isMatchAfter: true means appending after existing mapping (lower priority) |
addMappingForServletNames(dispatchers, isMatchAfter, names) | Match by Servlet name | Only intercept requests from the specified Servlet |
setInitParameter(k, v) | Set initialization parameters | exist init(FilterConfig) Read in |
dispatchers Detailed explanation of parameters (DispatcherType):
| DispatcherType | Trigger time | Typical scenario |
|---|---|---|
REQUEST | HTTP request initiated directly by the client | Most business filters (permissions, logs, encoding) |
FORWARD | request.getRequestDispatcher().forward() | Prevent Forward from repeatedly logging |
INCLUDE | request.getRequestDispatcher().include() | JSP <jsp:include> special treatment when |
ERROR | <error-page> Mechanism trigger | Special handling of error pages |
ASYNC | Asynchronous 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 interface | Trigger time | Typical uses |
|---|---|---|
ServletContextListener | contextInitialized() / contextDestroyed() | Application start/stopInitialize/destroy global resources (DataSource, thread pool) when |
ServletContextAttributeListener | When adding/deleting/modifying attributes | Monitor changes to global context properties |
Category 2: HttpSession life cycle (session level)
| Listener interface | Trigger time | Typical uses |
|---|---|---|
HttpSessionListener | sessionCreated() / sessionDestroyed() | Online user statistics,Session timeout cleaning |
HttpSessionAttributeListener | When adding/deleting/modifying session attributes | Monitor data changes in user sessions |
HttpSessionActivationListener | During passivation/activation | Session migration processing in distributed environment |
HttpSessionBindingListener | When binding/unbinding objects | Notify that the object itself is put into/out of the Session |
Category 3: ServletRequest life cycle (request level)
| Listener interface | Trigger time | Typical uses |
|---|---|---|
ServletRequestListener | requestInitialized() / requestDestroyed() | Request time-consuming statistics, MDC log context injection |
ServletRequestAttributeListener | When requesting attribute addition/deletion/modification | Audit 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
| Dimensions | Servlet registration | Filter registration | Listener registration |
|---|---|---|---|
| Registration method | addServlet(name, instance) | addFilter(name, instance) | addListener(instance) |
| Return type | ServletRegistration.Dynamic | FilterRegistration.Dynamic | void(no return value) |
| Configuration content | URL mapping, startup sequence, asynchronous support, initialization parameters | URL pattern matching, Servlet name matching, request type | None (the type is determined by the interface) |
| execution timing | Request processing (doGet/doPost) | Request interception (doFilter) | Life cycle callbacks (initialization/destruction/creation/destination) |
| Typical applications | DispatcherServlet, REST API Servlet | Coding, security, logging, performance monitoring | Global resource initialization, online statistics, request audit |
| Multiple instance relationships | Process requests independently without interfering with each other | Form FilterChain and execute sequentially | Each monitors different events and does not depend on each other. |
| Spring encapsulation | DispatcherServletRegistrationBean | FilterRegistrationBean | ContextLoaderListener(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
AnnotationAwareOrderComparatorsorting, support@OrderandOrderedinterface
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
@Controllermiddle@Autowired UserServicecan 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?
| benefit | illustrate |
|---|---|
| Hierarchical isolation | The 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 sharing | A 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 correct | The 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 test | The 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:
| step | illustrate | key code |
|---|---|---|
| ① | Tomcat scans SPI files in jar packages | META-INF/services/javax.servlet... |
| ② | Instantiate Spring entry class | SpringServletContainerInitializer |
| ③ | collect WebApplicationInitializer All subtypes | @HandlesTypes(WebApplicationInitializer.class) |
| ④ | Sorting, instantiation, calling one by one | AnnotationAwareOrderComparator.sort() |
| ⑤ | First call the parent class to initialize the Root Context | super.onStartup(servletContext) |
| ⑥ | Create Servlet Context + Register DispatcherServlet | registerDispatcherServlet() |
| ⑦ | Programmatic injection into Servlet container | servletContext.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
AbstractAnnotationConfigDispatcherServletInitializerSeveral 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/HttpServletResponseObjects, 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
| question | illustrate | solution |
|---|---|---|
| Duplicate registration | Use at the same time web.xml and WebApplicationInitializer | Choose one, pure Java method is recommended |
/ and /* Confuse | / Is the default Servlet mapping;/* Will intercept JSP | use /, for static resources DefaultServletHttpRequestHandler |
| Use the teaching demo as a production template | The 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 abuse | Unnecessary multiple DispatcherServlets lead to duplicate configuration and scattered exception handling | In 99% of scenarios, one DispatcherServlet is enough, and the code can be organized through modular @Controller subcontracting |
| Async support mismatch | DispatcherServlet enables asynchronous but Filter does not support it | Make sure all Filters also call setAsyncSupported(true) |
| Root Context scope | Root Context beans cannot be overwritten by Servlet Context | Root is placed in the global bean (Service/DAO), Servlet is placed in the Web Bean (Controller) |
| Startup performance | @HandlesTypes triggers full classpath scan | Using Spring Boot's auto-configuration can significantly reduce the scope of scanning |
| Exception causes startup failure | An Initializer's onStartup() throws an exception | Do it in every initializer try-catch and log |
| Maven packaging missing SPI files | Resource 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
| Dimensions | Traditional Spring MVC | Spring Boot |
|---|---|---|
| Configuration method | inherit AbstractAnnotationConfigDispatcherServletInitializer | use application.properties + @SpringBootApplication |
| container | External Tomcat/Jetty, create WAR package | Embed Tomcat/Jetty/Undertow and create JAR package |
| DispatcherServlet Create | pass createDispatcherServlet() Create manually | Pass automatically DispatcherServletAutoConfiguration create |
| Custom DispatcherServlet | overwrite createDispatcherServlet() | accomplish WebMvcRegistrations Interface or injection DispatcherServletRegistrationBean |
| Servlet Get | servletContext.addServlet() | DispatcherServletRegistrationBean (ServletContextInitializer) |
| start trigger | External container scans SPI files | ServletWebServerApplicationContext 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
| project | Java EE 8(Spring 5.x) | Jakarta EE 9+(Spring 6.x / Boot 3.x) |
|---|---|---|
| package name | javax.servlet.* | jakarta.servlet.* |
| SPI file path | META-INF/services/javax.servlet.ServletContainerInitializer | META-INF/services/jakarta.servlet.ServletContainerInitializer |
| Servlet container | Tomcat 9- / Jetty 9 | Tomcat 10+ / Jetty 11+ |
| ApplicationContext | AnnotationConfigWebApplicationContext | AnnotationConfigWebApplicationContext(The package path remains unchanged) |
Migration steps (3 minutes to complete):
- Global search and replace:
javax.servlet→jakarta.servlet - Ensure SPI file paths are updated simultaneously
- Upgrade to Tomcat 10+ or Jetty 11+
- 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
SpringServletContainerInitializercopy) 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 codespring-webModule:
org.springframework.web.SpringServletContainerInitializerorg.springframework.web.WebApplicationInitializerorg.springframework.web.servlet.support.AbstractDispatcherServletInitializerorg.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializerorg.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration(Boot version)