Spring 源码深度剖析:DispatcherServlet 的自动化注册机制

13 阅读35分钟

Spring 源码深度剖析:DispatcherServlet 的自动化注册机制

学习路径:SPI 机制 → Spring 实现 → 继承链 → 注册流程 → Spring Boot 差异 → 最佳实践 配套源码gitee.com/chouchouxia…


一、背景知识:Servlet 容器启动流程演进

在构建 Java Web 应用时,开发者面对的第一个问题往往是如何让请求入口组件被 Web 容器正确识别。

Servlet 2.x 时代(web.xml 时代):

  • 所有 Servlet、Filter、Listener 必须在 web.xml 中显式声明
  • 每次添加功能都需要修改部署描述符,耦合度高
  • 框架集成需要编写大量模板式 XML

Servlet 3.0+(零 XML/编程式注册时代):

  • 引入 javax.servlet.ServletContainerInitializer SPI 机制
  • 容器启动时自动扫描 classpath 下所有 jar 包的 META-INF/services/ 目录
  • 框架可以通过编程方式向容器注册 Servlet/Filter/Listener
  • 完全脱离 XML,拥抱类型安全

Servlet 3.0 引入的编程式注册 API:

// Servlet 注册 API
ServletRegistration.Dynamic reg = servletContext.addServlet("name", servletInstance);
reg.addMapping("/path");                 // URL 映射
reg.setLoadOnStartup(1);                 // 启动时加载(>=0 表示在容器启动时初始化)
reg.setAsyncSupported(true);             // 异步支持
reg.setInitParameter("key", "value");    // 初始化参数

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

// Listener 注册 API
servletContext.addListener(listenerInstance);

1.0 Servlet 基础概念问答

在深入源码之前,我们需要先回答几个最基础但又至关重要的问题。这些问题贯穿了整个 Servlet 和 Spring MVC 的设计思想。


Q1:我们为什么需要 Servlet,Servlet 里到底干了什么?

Servlet 是 Java Web 的核心基石,它解决了HTTP 是无状态协议,但我们需要有状态的业务处理这个问题。

简单说:浏览器发来一个 HTTP 请求(一串文本),Servlet 容器把它解析成 Java 对象,让你用 Java 代码处理,再把结果转换回 HTTP 响应发回去。

Servlet 内部的核心流程只有 4 步:

步骤组件干了什么
① 接收HttpServletRequest把 HTTP 请求解析成对象:URL、参数、Header、Cookie、Body
② 处理你的 doGet()/doPost()Java 业务逻辑(查数据库、调 Service、拼结果)
③ 响应HttpServletResponse设置状态码、Header,往输出流写内容
④ 封装Servlet 容器(Tomcat)resp 对象序列化成标准 HTTP 响应报文发回

以项目中的 HelloServlet 为例(配套源码):

// 1. 容器调用 service() → 根据 HTTP 方法分发给 doGet()
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    // 2. 设置响应头
    resp.setContentType("text/html;charset=UTF-8");
    // 3. 获取输出流,写 HTML
    PrintWriter out = resp.getWriter();
    out.println("<h1>Hello World</h1>");
    // 4. 流关闭后,容器把内容打包成 HTTP 响应发回浏览器
}

如果没有 Servlet,你需要在 Java 里手动解析原始 HTTP 报文:

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

你得自己写 Socket、解析字符串、处理编码、管理线程池、遵循 HTTP 规范——这些脏活累活全被 Servlet 容器帮你干了。

DispatcheServlet 在此基础上做了什么?它本质是一个超级 Servlet,把请求处理拆成了一套流水线:

请求 → HandlerMapping(找哪个 Controller)
     → HandlerAdapter(调用 Controller 方法)
     → Controller 执行(你的 @RequestMapping 方法)
     → ViewResolver(找 JSP / JSON 视图)
     → 响应

Spring MVC 不是"替代"Servlet,而是站在 Servlet 的肩膀上做了一层更高层的抽象,让你不用写 doGet(),写 @RequestMapping 就够了。

延伸:DispatcherServlet 和 Servlet 到底有什么区别?

Servlet 是接口规范,DispatcherServlet 是 Spring 对它的一个具体实现。 用继承链来看:

Servlet (接口 — Java EE 规范)
  └── GenericServlet (抽象类,协议无关)
        └── HttpServlet (抽象类,HTTP 协议)
              └── HttpServletBean (Spring 的接入点)
                    └── FrameworkServlet (Spring 容器感知)
                          └── DispatcherServlet ★

核心区别一句话:Servlet 只给你一个空的 doGet()/doPost(),剩下的全得你自己写;DispatcherServlet 在 Servlet 之上盖了一座"分发工厂",把请求自动路由给 @Controller

维度普通 ServletDispatcherServlet
如何处理请求你自己在 doGet()/doPost() 里写死逻辑请求进入 → HandlerMapping 找处理器 → HandlerAdapter 调用 @Controller 方法 → 返回响应
URL 路由@WebServlet("/user"),一个 Servlet 对应固定路径@RequestMapping("/user/{id}"),支持路径变量、RESTful 风格
参数绑定手动 req.getParameter("name")自动将请求参数绑定到方法参数(@RequestParam@PathVariable@RequestBody
响应处理手动 resp.getWriter().print(json)方法返回对象 → HttpMessageConverter 自动转 JSON/XML
扩展性无内置扩展点HandlerMapping / HandlerAdapter / Interceptor / ViewResolver 全程可插拔

简单类比:

// 普通 Servlet:手工作坊,每一步都自己干
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    String id = req.getParameter("id");        // 手动取参数
    User user = userService.findById(id);       // 手动调 Service
    resp.setContentType("application/json");
    resp.getWriter().write(toJson(user));       // 手动序列化 JSON
}

// DispatcherServlet:你只写业务,剩下的框架全包
@GetMapping("/user/{id}")
@ResponseBody
public User getUser(@PathVariable Long id) {    // 参数自动绑定
    return userService.findById(id);            // 只需返回对象,JSON 自动生成
}

所以 DispatcherServlet 不是"替代"了 Servlet,而是站在 Servlet 的肩膀上做了一层更高层的抽象——它自己本身就是一个 Servlet(继承了 HttpServlet),但把请求处理拆成了 HandlerMapping → HandlerAdapter → ViewResolver 这条流水线,让你从写 doGet() 变成写 @RequestMapping


Q2:Servlet 容器后面跟着(Tomcat)是什么意思?Servlet 跟 Tomcat 是什么关系?

Servlet 是接口规范,Tomcat 是实现者。

概念类比
Servlet 规范(javax.servlet.*USB 接口标准(规定形状、协议)
Tomcat具体的 USB 硬件(实现了接口,能插拔工作)

更具体地说:

  • Servlet 只是一套 Java 接口(javax.servlet.Servletjavax.servlet.ServletContext 等),规定了"怎么接收请求、怎么返回响应",但接口本身干不了活。
  • Tomcat 实现了这套接口,负责真正的工作:监听 8080 端口、接收 HTTP 连接、解析 HTTP 报文、构造对象、调用你的方法、序列化响应。

所以你写的 Servlet 代码从没见过 Socket、没处理过 TCP 连接——全是 Tomcat 在背后做的。

在项目的 pom.xml 中:

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

scope=provided 的意思是:编译时需要,但不打进去。因为 Tomcat 自带这份实现,运行时直接用它自带的就行。

其他 Servlet 容器也不止 Tomcat:

容器特点
Tomcat最主流,轻量
Jetty更轻量,适合嵌入
UndertowWildFly 默认,高性能
WebLogic / WebSphere企业级,商业授权

不管用哪个容器,你的 Servlet 代码都一样——因为都遵循同一套 javax.servlet 规范接口。


Q3:Tomcat 中构造的 HttpServletRequest 如何传给我写的 Java 程序,我的程序里构造的 HttpServletResponse 又如何传给 Tomcat?

本质就是一次普通的方法调用参数传递,跟你写 void foo(List list) { list.add("hello"); } 完全一样的逻辑:

浏览器 ──HTTP──▶ Tomcat ──Java对象──▶ 你的 Servlet
  ◀──HTTP──                    ◀──Java对象──

拆开来看完整流程:

Step 1: Tomcat 收到原始 HTTP 报文
  GET /hello HTTP/1.1
  Host: localhost:8080

Step 2: Tomcat 内部构造对象
  RequestFacade req  ─── 包装了 URL、Header、参数
  ResponseFacade resp ─── 空的,等着你来填

Step 3: Tomcat 调用你的代码(一次普通 Java 方法调用)
  helloServlet.service(req, resp);
    └─→ doGet(req, resp) {
          resp.setContentType("text/html");   // 往 resp 里写 Header
          resp.getWriter().write("<h1>...</h1>");  // 往 resp 里写 Body
        }

Step 4: 你的方法 return 后,Tomcat 从 resp 里取出内容

Step 5: Tomcat 序列化成 HTTP 响应报文发回
  HTTP/1.1 200 OK
  Content-Type: text/html;charset=UTF-8
  
  <h1>Hello World</h1>

为什么要用 Facade(门面)?

Tomcat 内部的真实对象是 org.apache.catalina.connector.Request,但它不直接把内部对象传给你,而是包了一层 RequestFacade(实现了 HttpServletRequest 接口):

Tomcat 内部:  Request ──→ RequestFacade ──→ 你的 Servlet.doGet(req)
                        (只暴露接口方法)

这样做有两个目的:

  1. 安全:你不能调用 Request 里 Tomcat 的内部方法,比如 request.getConnector().stop() 把服务器关了
  2. 隔离:将来 Tomcat 升级改内部实现,只要接口不变,你的代码不用改

一张图总结整个数据流:

            Tomcat 内部                       你的代码(Servlet)
           
   Socket 监听端口 8080
        │
        ▼
   解析 HTTP 报文
        │
        ▼
   new Request()  ──包装──▶  RequestFacade ──▶  doGet(req, resp)
   new Response() ◀──包装──  ResponseFacade ◀──  (你写 resp)
        │
        ▼
   序列化成 HTTP 响应
        │
        ▼
   发回浏览器

Q4:为什么 Tomcat 可以直接调用我的 Java 代码中的函数?

因为 Tomcat 本身就是 Java 程序,你的代码和 Tomcat 跑在同一个 JVM 进程里。

┌─────────────────── 同一个 Java 进程 (JVM) ───────────────────┐
│                                                               │
│   Tomcat 自己的代码:                                           │
│   Connector.java  →  "收到请求了,调用哪个 Servlet 来着?"        │
│                          │                                     │
│                          │ 普通 Java 方法调用                    │
│                          ▼                                     │
│   你的 Servlet 代码:                                           │
│   HelloServlet.doGet(req, resp) { ... }                       │
│                                                               │
└───────────────────────────────────────────────────────────────┘

这不是两个独立程序之间的 IPC 调用,而是同一个 JVM 里,A 类调用 B 类的方法。Tomcat 通过以下三步找到你的代码:

Step 1:Tomcat 通过 SPI 发现你的类。 项目中的 MyServletContainerInitializer 被 Tomcat 扫描到后,用反射实例化:

// Tomcat 本质上就是这样做:
AppInitializer initializer = (AppInitializer)
    clazz.getDeclaredConstructor().newInstance();  // 反射实例化
initializer.onStartup(ctx);  // 普通方法调用

Step 2:你把自己写的 Servlet 注册到 Tomcat 手里。HelloServletAppInitializer 中:

// 你 new HelloServlet() 创建一个对象,交给 Tomcat 存起来
// 这跟 map.put("hello", myServlet) 没本质区别
ServletRegistration.Dynamic reg = ctx.addServlet("hello", new HelloServlet());
reg.addMapping("/hello");

Step 3:请求来了,Tomcat 调你的方法。

Tomcat 线程收到 GET /hello
  → 查内部 Map: "/hello" 对应的是哪个 Servlet?
  → 找到你之前注册的 HelloServlet 对象
  → hello.servlet(req, resp)          ← 一次普通方法调用
    → hello.doGet(req, resp)          ← 你写的业务逻辑
  → 返回

类比一下:你写了一个 Calculator 类,然后一个 Main 类去调它——Tomcat 就是那个 Main,你的 Servlet 就是那个 Calculator,区别只是 Tomcat 用了反射来 newInstance()


Q5:为什么传统的 WAR 项目没有 Spring Boot 那样的启动类?

这是两种完全不同的启动模式:

传统 WAR 项目(本项目)Spring Boot
谁先启动Tomcat 先启动,再加载你的 WAR你的 main() 先启动,再内嵌启动 Tomcat
main 方法在哪在 Tomcat 的 Bootstrap 类里在你自己的启动类里
打包方式warjar(Fat JAR,内嵌 Tomcat)

本项目 pom.xml 里写的是 <packaging>war</packaging>,编译后产出 dispatcher-servlet-demo.war。把这个 WAR 丢到 Tomcat 的 webapps 目录下:

$CATALINA_HOME/bin/startup.sh   →   启动 Tomcat (Bootstrap.main())
  → 扫描 webapps 下的 WAR
    → 发现你项目里的 ServletContainerInitializer
      → 调用 onStartup() 注册你的 Servlet

而 Spring Boot 把这个关系颠倒了:

你项目的 main() 先启动
  → new SpringApplication().run()
    → 内嵌启动 Tomcat(Tomcat 变成了你项目里的一个库)
      → 再注册 DispatcherServlet

用图表示就是:

WAR 模式:                    Spring Boot 模式:

┌──Tomcat 主进程────────┐    ┌──你的 main()─────────┐
│  Bootstrap.main()     │    │  SpringApplication   │
│    │                  │    │    │                 │
│    ├─ 加载你的 WAR ──►│    │    ├─ 内嵌 Tomcat ◄──│── Tomcat 是你的依赖库
│    └─ 调你的 Servlet   │    │    └─ 注册 Servlet   │
└───────────────────────┘    └──────────────────────┘

所以在传统 WAR 项目中,"启动类"确实可以理解为就是 Tomcat 自己的 main()。Spring Boot 为了让开发更方便,把这个关系反过来了,让你自己掌握启动入口。


Q6:当 DispatcherServlet 映射到 /,另一个 ApiServlet 映射到 /api/* 时,/api/info 请求会交给谁处理?

这是一个关键的优先级规则问题。以本项目为例,存在两个 Servlet:

Servlet映射路径注册来源
DispatcherServlet/Spring MVC(MySpringMvcInitializer
ApiServlet/api/*自定义 SPI(ApiServletAppInitializer

同时 HomeController 中还有 @RequestMapping("/api/info")

GET /api/info 到达 Tomcat 时,容器层面的匹配规则如下(Servlet 规范定义,优先级从高到低):

请求: /api/info

Step 1: 精确匹配 → 有没有 Servlet 精确映射到 "/api/info" ?
  → 没有

Step 2: 路径前缀匹配(最长优先)→ 有没有 Servlet 映射到 "/api/*" ?
  → "/api/*" 匹配!→ ApiServlet 胜出 ✅

Step 3: 扩展名匹配 → *.jsp 等

Step 4: 默认匹配 → "/"(只有前面都没命中才轮到)
优先级匹配方式示例命中的请求谁赢
1精确路径/api/info只有 /api/info
2路径前缀(最长优先)/api/*/api/xxx/api/infoApiServlet
3扩展名*.jsp/xxx.jsp不适用
4默认 Servlet/兜底,匹配所有未命中请求DispatcherServlet(未轮到)

结论/api/* 的优先级高于 //api/info 在第二步就被 ApiServlet 拦截了,根本没机会到达 DispatcherServlet。所以 HomeController.info()@RequestMapping("/api/info") 永远不会被调用。

这意味着容器先决定由哪个 Servlet 处理请求,然后 Spring MVC 才能做内部路由。DispatcherServlet 能处理的只有那些没有被更精确 Servlet 拦截的请求。这是设计项目时必须注意的 —— 多个 Servlet 的映射路径不要互相冲突。


Q7:Controller 中的 @RequestMapping 本质上是另一个 Servlet 吗?

不是。 @RequestMapping 只是一个普通 Java 方法上的注解标记,完全不是 Servlet。

HomeController 为例:

@Controller
public class HomeController {          // ← 没有 extends HttpServlet

    @RequestMapping("/")              // ← 只是一个注解标记
    public String home(Model model) {
        return "home";
    }
}

HomeController 没有实现 HttpServlet,没有 service(req, resp) 方法,Tomcat 根本不认识它。整个项目中,Tomcat 只认识那一个实现 HttpServlet 的入口 —— DispatcherServlet

实际的调用链是这样的:

请求 GET /
  │
  ▼
Tomcat: "匹配 '/' → 找到 DispatcherServlet(因为它映射到 /)"
  │
  ▼
DispatcherServlet.doDispatch()         ← ★ 只有这一步是 Servlet
  │
  ├── 拿到 req, resp(Tomcat 传进来的)
  ├── HandlerMapping: "请求路径是 /,谁处理?→ HomeController.home()"
  ├── HandlerAdapter: "调这个方法 → home(Model model)"
  │       └── model.addAttribute(...)   ← 普通 Java 方法调用(反射)
  │       └── return "home"
  ├── ViewResolver: "home" → /WEB-INF/views/home.jsp
  └── 渲染 JSP,写入 resp

类比一下:

Tomcat = 大楼前台(只认识一个入口)
DispatcherServlet = 前台小哥(唯一的 Servlet)
@RequestMapping = 公司内部工位号(Spring 自己用的,Tomcat 不知道)

所以 @RequestMapping 是 Spring MVC 在 DispatcherServlet 这个"超级 Servlet"内部做二级路由的标记。对 Tomcat 来说,从头到尾只有一个 DispatcherServlet 在跟它打交道。

Q8:MySpringMvcInitializer 跟 HelloServletAppInitializer、ApiServletAppInitializer 做的事情是不是差不多?

它们在 SPI 发现机制层面确实是同一套体系,但做的事情层级完全不同。

三者最终都会被 ServletContainerInitializer 通过 @HandlesTypes 发现并调用 onStartup(ServletContext)。流程都是:

容器启动
  → 扫描 META-INF/services/javax.servlet.ServletContainerInitializer
    → @HandlesTypes(WebApplicationInitializer.class)
      → 找到所有实现类 → 调用 onStartup()

但关键区别在于:

维度MySpringMvcInitializerHelloServlet/ApiServlet Initializer
继承链WebApplicationInitializerAbstractContextLoaderInitializerAbstractDispatcherServletInitializerAbstractAnnotationConfigDispatcherServletInitializer直接实现 AppInitializer(等同于 WebApplicationInitializer
做的事情启动整套 Spring 容器(创建 Root + Servlet 两个 ApplicationContext,注册 DispatcherServlet)直接注册原生 Servlet + Filter
请求处理方式走 Spring MVC 的 DispatcherServlet → HandlerMapping → HandlerAdapter → @Controller 完整链路直接由 Servlet 的 service() / doGet() 处理,没有 Spring 参与
配置方式通过 getRootConfigClasses() / getServletConfigClasses() 返回 @Configuration直接在 onStartup() 中用 addServlet() / addFilter() 编程式注册

简单类比:

// HelloServletAppInitializer:手工作坊,自己注册原始 Servlet
ctx.addServlet("hello", new HelloServlet());  // 直接 Servlet,不走 Spring

// MySpringMvcInitializer:盖了一座工厂,把整个 Spring MVC 拉起来
getRootConfigClasses();     // → 创建 Root ApplicationContext(Service/DAO 层)
getServletConfigClasses();  // → 创建 Servlet ApplicationContext(Controller 层)
// 最终注册的是 DispatcherServlet,由它统一分发请求给 @Controller

但要注意"启动"这个词的准确性:Spring MVC 并不是"先启动再加载 MySpringMvcInitializer"。实际发生的顺序是:

① Tomcat 容器启动
   ↓
② Tomcat 扫描 SpringServletContainerInitializer(Spring 提供的 SPI 入口)
   ↓
③ SpringServletContainerInitializer 通过 @HandlesTypes 发现 MySpringMvcInitializer
   ↓
④ 反射创建 MySpringMvcInitializer 实例,调用 onStartup()
   ↓
⑤ onStartup() 内部才开始创建 Spring 容器:
     → 创建 Root ApplicationContext(Service/DAO 层)
     → 创建 Servlet ApplicationContext(Controller 层)
     → 注册 DispatcherServlet 到 Tomcat

此时 Spring MVC 才是第一次诞生。说白了:MySpringMvcInitializer 是"点火器",Tomcat 是"打火机"——Tomcat 先转动打火机(触发 SPI),但火花真正燃起 Spring MVC 这颗引擎的是 MySpringMvcInitializer.onStartup()

教学说明:本工程的示例代码(HelloServletAppInitializerApiServletAppInitializerListenerDemoAppInitializer)为了教学清晰度而拆成了多个独立的 AppInitializer 实现类——每个类专注演示一种注册方式(Servlet、Filter、Listener),方便读者分开理解。实际项目中不会这样拆分,通常是一个 MySpringMvcInitializer 囊括所有注册逻辑(Servlet + Filter + Listener),或者直接用 Spring Boot 自动配置全部搞定。


1.1 Servlet 注册 API 详解

理论说明:

ServletRegistration.Dynamic 是 Servlet 3.0 引入的编程式注册接口,返回的 Dynamic 对象允许在初始化阶段(容器调用 onStartup() 期间)动态配置 Servlet 的元数据,从而完全替代 web.xml 中的 <servlet><servlet-mapping> 声明。

核心方法的职责:

方法作用类比 web.xml 元素调用时机
addServlet(name, servlet)向容器注册一个 Servlet 实例,返回注册句柄<servlet><servlet-name>必须在容器初始化阶段(onStartup 内)调用
addMapping(patterns)指定 URL 路径映射规则<servlet-mapping><url-pattern>注册后立即配置
setLoadOnStartup(int)设置启动加载优先级。>=0 表示容器启动时初始化;<0 或省略表示首次请求时懒加载<load-on-startup>注册后
setAsyncSupported(boolean)是否支持异步 Servlet 处理(Servlet 3.0 新特性)<async-supported>注册后
setInitParameter(k, v)设置 Servlet 初始化参数,可在 getInitParameter() 中读取<init-param>注册后

关键约束:

  • addServlet() 只能被调用一次(每个 name 唯一),重复调用会返回 null
  • 所有配置必须在容器调用 onStartup() 返回之前完成,之后 Dynamic 对象变为只读
  • setLoadOnStartup(1) 的值越小优先级越高,负值表示懒加载

实际举例 —— Spring 中 DispatcherServlet 的注册:

// 源码出处:AbstractDispatcherServletInitializer.registerDispatcherServlet()
// 这是 Spring MVC 框架内部对 Servlet 注册 API 的实际使用

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

    // ★ 核心:通过编程式 API 注册 DispatcherServlet
    ServletRegistration.Dynamic registration =
            servletContext.addServlet(servletName, dispatcherServlet);

    // 配置 Servlet 行为
    registration.setLoadOnStartup(1);                       // 容器启动时立即初始化
    registration.addMapping(getServletMappings());          // → "/" 默认映射
    registration.setAsyncSupported(isAsyncSupported());     // → true 异步支持

    // 可选的自定义配置(钩子方法,子类可覆写)
    customizeRegistration(registration);
}

实际举例 —— 自定义业务场景(多个 Servlet 隔离):

// 场景:一个 Web 应用同时提供页面浏览和 REST API 服务,
// 用两个不同的 Servlet 进行隔离

public class MultiServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext ctx) {
        // --- Servlet 1:页面 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);          // 优先级低于页面 Servlet
        apiServlet.setAsyncSupported(true);      // API 支持异步

        // --- Servlet 3:健康检查 Servlet(懒加载) ---
        ServletRegistration.Dynamic healthServlet =
                ctx.addServlet("health", new HealthServlet());
        healthServlet.addMapping("/health");
        // 不调用 setLoadOnStartup() → 首次请求时才初始化
    }
}

1.2 Filter 注册 API 详解

理论说明:

FilterRegistration.Dynamic 提供了编程式注册 Filter 的能力,其核心价值在于:

  1. 精确控制拦截范围 —— 通过 addMappingForUrlPatterns()addMappingForServletNames() 两种方式
  2. 控制执行顺序 —— 通过 setInitParameter()EnumSet<DispatcherType> 参数
  3. 完全替代 web.xml 中的 <filter><filter-mapping>

核心方法:

方法作用关键参数说明
addFilter(name, filter)注册 Filter 实例name 在同一上下文中唯一
addMappingForUrlPatterns(dispatchers, isMatchAfter, patterns)按 URL 路径匹配dispatchers:请求类型枚举(REQUEST/FORWARD/INCLUDE/ERROR/ASYNC);isMatchAfter:true 表示追加到已有映射之后(优先级更低)
addMappingForServletNames(dispatchers, isMatchAfter, names)按 Servlet 名称匹配只拦截指定 Servlet 的请求
setInitParameter(k, v)设置初始化参数init(FilterConfig) 中读取

dispatchers 参数(DispatcherType)详解:

DispatcherType触发时机典型场景
REQUEST客户端直接发起的 HTTP 请求绝大多数业务 Filter(权限、日志、编码)
FORWARDrequest.getRequestDispatcher().forward()防止 Forward 重复记录日志
INCLUDErequest.getRequestDispatcher().include()JSP <jsp:include> 时的特殊处理
ERROR<error-page> 机制触发错误页面的特殊处理
ASYNC异步 Servlet 的 startAsync()异步请求的上下文清理

实际举例 —— Spring 中 Filter 的注册机制:

// 源码出处:AbstractDispatcherServletInitializer.registerServletFilter()
// Spring 将 Filter 注册到 DispatcherServlet 的核心逻辑

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

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

    // 设置 Filter 拦截所有 DispatcherServlet 处理的请求
    // REQUEST + FORWARD + INCLUDE + ASYNC:覆盖所有常见请求类型
    registration.addMappingForUrlPatterns(
            EnumSet.of(DispatcherType.REQUEST,
                       DispatcherType.FORWARD,
                       DispatcherType.INCLUDE,
                       DispatcherType.ASYNC),
            false,                          // false = 添加到映射列表最前面(高优先级)
            getServletMappings());          // → 与 DispatcherServlet 的映射路径一致
    return registration;
}

实际举例 —— 企业级 Filter 链编排:

// 场景:多个 Filter 的注册顺序和执行顺序控制

public class FilterChainInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext ctx) {
        // ★ 注册顺序 = 执行顺序(对于同一种 mapping 方式)

        // 第1个注册:字符编码 Filter(最先执行)
        FilterRegistration.Dynamic encodingFilter =
                ctx.addFilter("encoding", new CharacterEncodingFilter("UTF-8"));
        encodingFilter.addMappingForUrlPatterns(
                EnumSet.of(DispatcherType.REQUEST), true, "/*");

        // 第2个注册:安全认证 Filter(第二个执行)
        FilterRegistration.Dynamic securityFilter =
                ctx.addFilter("security", new SecurityFilter());
        securityFilter.addMappingForUrlPatterns(
                EnumSet.of(DispatcherType.REQUEST), true, "/admin/*");

        // 第3个注册:请求日志 Filter(第三个执行)
        FilterRegistration.Dynamic logFilter =
                ctx.addFilter("logging", new LoggingFilter());
        logFilter.addMappingForUrlPatterns(
                EnumSet.of(DispatcherType.REQUEST), true, "/*");

        // 通过 Servlet 名称精确匹配(仅拦截 "api" Servlet 的请求)
        FilterRegistration.Dynamic apiAuthFilter =
                ctx.addFilter("apiAuth", new ApiAuthFilter());
        apiAuthFilter.addMappingForServletNames(
                EnumSet.of(DispatcherType.REQUEST), true, "api");
    }
}

1.3 Listener 注册 API 详解

理论说明:

ServletContext.addListener() 是三个 API 中最简洁的 —— 只需要传入 Listener 实例,容器会自动根据 Listener 实现的接口类型来判断其用途。Servlet 3.0 规范定义了 8 种核心 Listener 接口,按监听对象分为三类:

第一类:ServletContext 生命周期(应用级别)

Listener 接口触发时机典型用途
ServletContextListenercontextInitialized() / contextDestroyed()应用启动/关闭时初始化/销毁全局资源(DataSource、线程池)
ServletContextAttributeListener属性增/删/改时监控全局上下文属性的变更

第二类:HttpSession 生命周期(会话级别)

Listener 接口触发时机典型用途
HttpSessionListenersessionCreated() / sessionDestroyed()在线用户统计、Session 超时清理
HttpSessionAttributeListener会话属性增/删/改时监控用户会话中的数据变更
HttpSessionActivationListener钝化/活化时分布式环境下 Session 迁移处理
HttpSessionBindingListener对象绑定/解绑时通知对象自身被放入/移出 Session

第三类:ServletRequest 生命周期(请求级别)

Listener 接口触发时机典型用途
ServletRequestListenerrequestInitialized() / requestDestroyed()请求耗时统计、MDC 日志上下文注入
ServletRequestAttributeListener请求属性增/删/改时请求参数的审计追踪

实际举例 —— Spring 中 ContextLoaderListener 的使用:

// 源码出处:AbstractContextLoaderInitializer.registerContextLoaderListener()
// Spring 利用 ServletContextListener 来管理 Root WebApplicationContext 的生命周期

private void registerContextLoaderListener(ServletContext servletContext) {
    WebApplicationContext rootContext = createRootApplicationContext();
    if (rootContext != null) {
        // 注册 ContextLoaderListener → 它实现了 ServletContextListener
        // contextInitialized() → 创建 Root WebApplicationContext
        // contextDestroyed()  → 关闭 Root WebApplicationContext
        servletContext.addListener(new ContextLoaderListener(rootContext));
    }
}

// ContextLoaderListener 的内部简化逻辑:
public class ContextLoaderListener implements ServletContextListener {

    private WebApplicationContext context;

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

    @Override
    public void contextInitialized(ServletContextEvent event) {
        // ★ 应用启动时:初始化 Root 容器
        // 执行所有 Bean 的实例化、依赖注入、初始化方法
        ((ConfigurableWebApplicationContext) context).refresh();
        event.getServletContext().log("Root WebApplicationContext 初始化完成");
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        // ★ 应用关闭时:优雅关闭 Root 容器
        // 执行所有 @PreDestroy 回调、释放连接池、关闭线程池
        ((ConfigurableWebApplicationContext) context).close();
    }
}

实际举例 —— 企业级 Listener 组合应用:

// 场景:一个完整的 Web 应用需要启动时初始化、在线统计、请求审计

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

        // 1. 应用启动/关闭:初始化全局线程池 + 优雅关闭
        ctx.addListener(new AppLifecycleListener());

        // 2. 在线用户统计:记录当前活跃 Session 数
        ctx.addListener(new OnlineUserListener());

        // 3. 请求审计:每个请求的开始/结束打日志
        ctx.addListener(new RequestAuditListener());
    }
}

// ===== Listener 1:应用生命周期管理 =====
public class AppLifecycleListener implements ServletContextListener {
    private ScheduledExecutorService scheduler;

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // 初始化全局定时任务线程池
        scheduler = Executors.newScheduledThreadPool(4);
        sce.getServletContext().setAttribute("scheduler", scheduler);
        System.out.println("[App] 应用启动,线程池已初始化");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // 优雅关闭线程池
        if (scheduler != null) {
            scheduler.shutdown();
            System.out.println("[App] 应用关闭,线程池已优雅停止");
        }
    }
}

// ===== Listener 2:在线用户统计 =====
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] 新会话创建,当前在线: " + activeSessions);
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        activeSessions--;
        se.getSession().getServletContext()
                .setAttribute("onlineUsers", activeSessions);
        System.out.println("[Session] 会话销毁,当前在线: " + activeSessions);
    }
}

// ===== Listener 3:请求审计 =====
public class RequestAuditListener implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        // 在 MDC 中注入请求 ID,便于日志追踪
        String requestId = UUID.randomUUID().toString().substring(0, 8);
        MDC.put("reqId", requestId);
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        // 清理 MDC 上下文,防止内存泄漏
        MDC.clear();
    }
}

1.4 三大注册 API 对比总结

维度Servlet 注册Filter 注册Listener 注册
注册方法addServlet(name, instance)addFilter(name, instance)addListener(instance)
返回类型ServletRegistration.DynamicFilterRegistration.Dynamicvoid(无返回值)
配置内容URL 映射、启动顺序、异步支持、初始化参数URL 模式匹配、Servlet 名称匹配、请求类型无(类型由接口决定)
执行时机请求处理(doGet/doPost)请求拦截(doFilter)生命周期回调(初始化/销毁/创建/销毁)
典型应用DispatcherServlet、REST API Servlet编码、安全、日志、性能监控全局资源初始化、在线统计、请求审计
多实例关系独立处理请求,互不干扰形成 FilterChain,顺序执行各自监听不同事件,互不依赖
Spring 封装DispatcherServletRegistrationBeanFilterRegistrationBeanContextLoaderListener(ServletContextListener)

核心设计思想:

  • Servlet = 请求处理器(处理"做什么")
  • Filter = 请求拦截器(处理"在之前/之后做什么")
  • Listener = 事件监听器(处理"当什么发生时做什么")

这三者构成了 Servlet 容器的拦截器链 + 处理器 + 观察者的完整架构,而 Servlet 3.0 的编程式注册 API 让这些组件的装配完全脱离了 XML,实现了类型安全、IDE 友好的现代开发模式。


二、Servlet 3.0 的 SPI 约定详解

Servlet 3.0 规范定义了一条特殊文件路径,Servlet 容器(Tomcat、Jetty 等)必须在启动时扫描并解析它:

META-INF/services/javax.servlet.ServletContainerInitializer

2.1 核心接口

package javax.servlet;

import java.util.Set;

public interface ServletContainerInitializer {
    /**
     * 容器启动时自动调用此方法
     * @param c   @HandlesTypes 注解指定的类型集合(容器自动收集)
     * @param ctx ServletContext,用于编程式注册 Servlet/Filter/Listener
     */
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

@HandlesTypes 注解的作用:

@HandlesTypes(WebApplicationInitializer.class)
class MyInitializer implements ServletContainerInitializer {
    /**
     * 容器会扫描 classpath 下所有 WebApplicationInitializer 的子类型
     * 将它们收集到 Set<Class<?>> c 中传入
     * 注意:容器可能将接口、抽象类也塞入集合中,需要自行过滤
     */
}

2.2 SPI 文件加载流程(容器内部视角)

Tomcat 启动
  │
  ├── 扫描所有 jar 包 → 查找 META-INF/services/javax.servlet.ServletContainerInitializer
  │
  ├── 解析文件内容 → 获取完全限定类名列表
  │
  ├── 对每个类名执行:
  │     ├── 反射加载该类
  │     ├── 检查是否标注 @HandlesTypes
  │     │     └── 有 → 扫描 classpath 收集所有匹配的子类型
  │     │     └── 无 → c 集合为 null
  │     └── 实例化 → 调用 onStartup(c, servletContext)
  │
  └── SpringServletContainerInitializer.onStartup() 被调用
        → 获取所有 WebApplicationInitializer 实现
        → 排序 → 逐个实例化 → 执行 onStartup()

2.3 完整 SPI 演示代码

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;

// ============================================================
// 标记接口:凡是实现此接口的类,都会在容器启动时被自动发现
// ============================================================
interface AppInitializer {
    void onStartup(ServletContext ctx);
}

// ============================================================
// 具体初始化逻辑:编程方式注册一个 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 已通过编程方式注册");

        // 也可以注册 Filter
        FilterRegistration.Dynamic filterReg = ctx.addFilter("logging", new LoggingFilter());
        filterReg.addMappingForUrlPatterns(null, true, "/*");
    }
}

// ============================================================
// 一个简单的 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 自主注册成功!</h1>");
        out.println("<p>此 Servlet 未在 web.xml 中声明</p>");
        out.println("<p>通过 SPI 文件 + ServletContainerInitializer 自动发现并注册</p>");
    }
}

// ============================================================
// Filter 实现
// ============================================================
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() + " - 耗时: " + elapsed + "ms");
    }
    @Override public void init(FilterConfig config) {}
    @Override public void destroy() {}
}

// ============================================================
// SPI 入口类(配合 @HandlesTypes)
// 需在 META-INF/services/javax.servlet.ServletContainerInitializer 中声明
// ============================================================
@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] 未发现任何 AppInitializer 实现类");
            return;
        }

        for (Class<?> clazz : initializerClasses) {
            // 关键防御性检查:过滤接口、抽象类、非目标类型
            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() + " 初始化完毕");
            } catch (Exception e) {
                throw new ServletException("无法实例化 " + clazz.getName(), e);
            }
        }
    }
}

SPI 配置文件位置: src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer

文件内容(一行):

com.example.servlet3demo.MyServletContainerInitializer

三、Spring 框架的 ServletContainerInitializer 实现

Spring 在 spring-web 模块中内置了自己的 SPI 入口。打开 Spring 的 jar 包,可以看到:

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

org.springframework.web.SpringServletContainerInitializer

3.1 SpringServletContainerInitializer 源码分析

// 源码位置:org.springframework.web.SpringServletContainerInitializer
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

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

        // 第 1 步:收集所有非接口、非抽象、可实例化的类
        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);
                    }
                }
            }
        }

        // 第 2 步:空集合处理
        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }

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

        // 第 3 步:按 @Order 排序后依次执行
        AnnotationAwareOrderComparator.sort(initializers);
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }
}

关键设计要点:

  • 使用 @HandlesTypes(WebApplicationInitializer.class) 让容器自动收集所有实现类
  • 过滤接口和抽象类(防御性编程,某些 Servlet 容器不规范)
  • 使用 AnnotationAwareOrderComparator 排序,支持 @OrderOrdered 接口

3.2 WebApplicationInitializer 接口

// 源码位置:org.springframework.web.WebApplicationInitializer
public interface WebApplicationInitializer {
    /**
     * 在 Servlet 容器启动时由 SpringServletContainerInitializer 回调
     * 开发者在此方法中以编程方式注册 Servlet、Filter、Listener
     */
    void onStartup(ServletContext servletContext) throws ServletException;
}

四、继承链深度解析

为了降低开发难度,Spring 提供了三层抽象基类,形成一个清晰的责任链:

WebApplicationInitializer (接口)
  │  onStartup(ServletContext)
  │
  ├── AbstractContextLoaderInitializer (抽象类)
  │    职责: 创建 Root WebApplicationContext(Service/DAO 层)
  │    关键方法: createRootApplicationContext()
  │
  ├── AbstractDispatcherServletInitializer (抽象类)
  │    职责: 创建 Servlet WebApplicationContext(Controller 层)
  │          注册 DispatcherServlet
  │    关键方法:
  │      - createServletApplicationContext()
  │      - createDispatcherServlet()
  │      - getServletMappings()     ← 子类必须实现
  │      - getServletName()         ← 子类必须实现
  │      - isAsyncSupported()       ← 可覆写,默认 true
  │
  └── AbstractAnnotationConfigDispatcherServletInitializer (抽象类)
       职责: 基于注解配置,约定优于配置
       关键方法:
        - getRootConfigClasses()
        - getServletConfigClasses()

┌─ 你的实现类 ──────────────────────┐
│ extends AbstractAnnotationConfigDispatcherServletInitializer │
│ 只需实现 3-4 个抽象方法即可        │
└────────────────────────────────────┘

4.1 逐层源码剖析

// ============================================================
// 第一层:Root ApplicationContext 初始化
// 源码:org.springframework.web.context.AbstractContextLoaderInitializer
// ============================================================
abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 创建 Root WebApplicationContext(通常包含 DataSource、Service、DAO)
        registerContextLoaderListener(servletContext);
    }

    private void registerContextLoaderListener(ServletContext servletContext) {
        WebApplicationContext rootContext = createRootApplicationContext();
        if (rootContext != null) {
            // 注册 ContextLoaderListener,由它管理 Root Context 的生命周期
            servletContext.addListener(
                    new ContextLoaderListener(rootContext));
        } else {
            // rootContext 为 null 表示不需要 Root Context(如只有 Controller 没有 Service)
            logger.debug("No root context configured");
        }
    }

    // 子类可以覆写此方法来提供 Root Context
    @Nullable
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }
}

// ============================================================
// 第二层:DispatcherServlet 注册
// 源码:org.springframework.web.servlet.support.AbstractDispatcherServletInitializer
// ============================================================
abstract class AbstractDispatcherServletInitializer
        extends AbstractContextLoaderInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 先执行父类逻辑 → 初始化 Root Context
        super.onStartup(servletContext);

        // 再注册 DispatcherServlet
        registerDispatcherServlet(servletContext);
    }

    // ==================== 注册流程(模板方法模式) ====================
    protected void registerDispatcherServlet(ServletContext servletContext) {
        String servletName = getServletName();
        WebApplicationContext servletAppContext = createServletApplicationContext();

        // 创建 DispatcherServlet 实例(可被子类覆写)
        FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);

        // 编程式注册到 Servlet 容器
        ServletRegistration.Dynamic registration =
                servletContext.addServlet(servletName, dispatcherServlet);
        registration.setLoadOnStartup(1);
        registration.addMapping(getServletMappings());
        registration.setAsyncSupported(isAsyncSupported());

        // 注册额外的 Filter(如果有)
        Filter[] filters = getServletFilters();
        if (filters != null && filters.length > 0) {
            for (Filter filter : filters) {
                registerServletFilter(servletContext, filter);
            }
        }
    }

    // ==================== 可覆写的钩子方法 ====================
    protected FrameworkServlet createDispatcherServlet(
            WebApplicationContext servletAppContext) {
        return new DispatcherServlet(servletAppContext);
    }

    protected boolean isAsyncSupported() {
        return true;
    }

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

    // ==================== 子类必须实现的抽象方法 ====================
    protected abstract String getServletName();
    protected abstract String[] getServletMappings();
    protected abstract WebApplicationContext createServletApplicationContext();
}

// ============================================================
// 第三层:注解配置支持
// 源码:org.springframework.web.servlet.support
//       .AbstractAnnotationConfigDispatcherServletInitializer
// ============================================================
abstract class AbstractAnnotationConfigDispatcherServletInitializer
        extends AbstractDispatcherServletInitializer {

    @Override
    protected WebApplicationContext createServletApplicationContext() {
        // 创建 AnnotationConfigWebApplicationContext(注解驱动的 ApplicationContext)
        AnnotationConfigWebApplicationContext context =
                new AnnotationConfigWebApplicationContext();

        // 注册 @Configuration 类
        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;  // 没有 Root Config 则跳过
        }
        AnnotationConfigWebApplicationContext rootContext =
                new AnnotationConfigWebApplicationContext();
        rootContext.register(configClasses);
        return rootContext;
    }

    // 子类提供配置类即可
    @Nullable
    protected abstract Class<?>[] getRootConfigClasses();
    protected abstract Class<?>[] getServletConfigClasses();
}

4.2 子类的实际用法

// ============================================================
// 实际项目中的用法 —— 只需继承并实现抽象方法
// ============================================================
public class MyWebApplicationInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

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

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

    @Override
    protected boolean isAsyncSupported() {
        return true;                                 // 启用异步支持
    }
}

4.3 父子容器关系详解

Q:为什么说 Root ApplicationContext 对应 Service/DAO 层,Servlet ApplicationContext 对应 Controller 层?源码里哪里体现了这个关联?

阅读 4.1 节的源码时会发现,三个抽象类的代码并没有硬编码"Root = Service/DAO"、"Servlet = Controller"这样的对应关系。这种关联实际上由两个层面共同决定:

层面一:父子容器的架构设计

Spring MVC 采用双容器(Parent-Child)模型,两个 ApplicationContext 之间存在父子关系:

Root ApplicationContext (父容器)
  ├── DataSource
  ├── UserService (@Service)
  └── UserDao (@Repository)
       ↑
       │ 子容器通过 setParent() 关联父容器
       │ 查找 Bean 时:先搜自己 → 搜不到则向上搜父容器
       │
Servlet ApplicationContext (子容器)
  ├── UserController (@Controller)
  ├── ViewResolver
  └── HandlerMapping

关键机制:

  • 子容器可以访问父容器的 Bean —— 所以 @Controller@Autowired UserService 能注入成功
  • 父容器不能访问子容器的 Bean —— 所以 Service 层不会反向依赖 Web 层,保证分层干净
  • 当有多个 DispatcherServlet 时,它们共享同一个 Root Context,避免 Service/DAO 被重复加载

层面二:约定优于配置

源码本身没有限制哪个容器放什么 Bean,对应关系是通过你提供的 @Configuration 类来建立的:

// getRootConfigClasses() → 返回的配置类扫描了 Service/DAO 包
@Override
protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { RootConfig.class };
    // RootConfig 中:
    //   @ComponentScan("com.example.service")
    //   @ComponentScan("com.example.dao")
    //   → 这些扫描到的 Bean 就进了 Root Context
}

// getServletConfigClasses() → 返回的配置类扫描了 Controller 包
@Override
protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { WebConfig.class };
    // WebConfig 中:
    //   @ComponentScan("com.example.controller")
    //   @EnableWebMvc  // 注册 HandlerMapping、ViewResolver 等 MVC 基础设施
    //   → 这些扫描到的 Bean 就进了 Servlet Context
}

也就是说:Root Context 之所以"等于"Service/DAO 层,是因为你在 getRootConfigClasses() 的配置类里扫描了 Service/DAO 包;Servlet Context 之所以"等于"Controller 层,是同理。这是 Spring 官方推荐的分层实践,不是框架代码强制约束。

为什么要拆成两个容器?

好处说明
分层隔离Service 层不知道 Web 层的存在,不能反向依赖 Controller —— 架构更干净
多 Servlet 共享一个 Root Context 可被多个 Servlet Context 共享(如 admin-dispatcher + api-dispatcher 共享同一套 Service/DAO),避免 Bean 重复加载
事务边界正确事务管理器放在 Root Context 中,所有子容器共享同一个事务管理器,AOP 事务切面能正确包裹 Service 方法调用
便于测试可以单独加载 Root Context 测试 Service 层,无需启动 Web 层

详见 六、关键设计模式分析 → 6.2 父子 ApplicationContext 设计


五、完整时序图

  Servlet容器           SpringServletContainer         AbstractContext          AbstractDispatcher
  (Tomcat)              Initializer                    LoaderInitializer        ServletInitializer
     │                        │                             │                        │
     │  ① 扫描 META-INF/      │                             │                        │
     │  services/ 文件        │                             │                        │
     │────────────────────────▶                             │                        │
     │                        │                             │                        │
     │  ② 实例化并调用        │                             │                        │
     │  onStartup()           │                             │                        │
     │────────────────────────▶                             │                        │
     │                        │                             │                        │
     │  ③ @HandlesTypes       │                             │                        │
     │  收集 WebAppInit       │                             │                        │
     │  实现类并过滤排序      │                             │                        │
     │                        │                             │                        │
     │                        │  ④ 逐个调用 onStartup()     │                        │
     │                        │─────────────────────────────▶                        │
     │                        │                             │                        │
     │                        │                             │  ⑤ super.onStartup()  │
     │                        │                             │────────────────────────▶
     │                        │                             │                        │
     │                        │                             │  ⑥ registerDispatcher │
     │                        │                             │  Servlet()             │
     │                        │                             │◀────────────────────────
     │                        │                             │                        │
     │                        │                             │  ⑦ servletContext.     │
     │                        │                             │  addServlet() ★        │
     │                        │                             │────────► 容器注册      │
     │                        │                             │                        │
     │  ★ 新注册的 Servlet/   │                             │                        │
     │  Filter 生效           │                             │                        │
     │◀─────────────────────────────────────────────────────│                        │

时序图说明:

步骤说明关键代码
Tomcat 扫描 jar 包中的 SPI 文件META-INF/services/javax.servlet...
实例化 Spring 入口类SpringServletContainerInitializer
收集 WebApplicationInitializer 所有子类型@HandlesTypes(WebApplicationInitializer.class)
排序、实例化、逐个调用AnnotationAwareOrderComparator.sort()
先调用父类初始化 Root Contextsuper.onStartup(servletContext)
创建 Servlet Context + 注册 DispatcherServletregisterDispatcherServlet()
编程式注入到 Servlet 容器servletContext.addServlet(name, servlet)

六、关键设计模式分析

6.1 模板方法模式

AbstractContextLoaderInitializer.onStartup()
  └── createRootApplicationContext()        ← 抽象方法,子类实现

AbstractDispatcherServletInitializer.onStartup()
  └── super.onStartup()                     ← 调用父类
  └── registerDispatcherServlet()           ← 定义算法骨架
        ├── getServletName()                ← 抽象方法,子类实现
        ├── createServletApplicationContext() ← 抽象方法,子类实现
        ├── createDispatcherServlet()       ← 可覆写,默认创建 DispatcherServlet
        ├── getServletMappings()            ← 抽象方法,子类实现
        └── getServletFilters()             ← 可覆写,默认 null

为什么用模板方法模式?

  • 将固定的注册流程封装在基类中,子类只需提供变化的数据
  • 符合"开闭原则"——新增功能只需扩展子类,无需修改基类
  • 提供钩子方法(如 createDispatcherServlet())给高级用户做定制

6.2 父子 ApplicationContext 设计

// Root ApplicationContext ─── 全局共享 Bean
//   ├── DataSource
//   ├── @Service  Bean
//   └── @Repository Bean
//
// Servlet ApplicationContext ─── Web 层专用 Bean
//   ├── @Controller Bean
//   ├── ViewResolver
//   └── HandlerMapping
//
// 关系:
//   Servlet Context 可以访问 Root Context 的 Bean
//   Root Context 不能访问 Servlet Context 的 Bean
//   └── 这就是为什么事务管理器在 Root Context 中配置
//       而 Controller 在 Servlet Context 中配置

6.3 防御性编程

SpringServletContainerInitializer 中的三重过滤:

// 为什么要过滤接口和抽象类?
// 在某些老旧的 Servlet 容器实现中,@HandlesTypes 注解可能会将
// 接口本身和抽象类也加入到 Set<Class<?>> 中。
// 如果不加过滤,直接 clazz.newInstance() 会抛出 InstantiationException
if (!waiClass.isInterface()                       // 排除接口
        && !Modifier.isAbstract(waiClass.getModifiers())  // 排除抽象类
        && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { // 类型校验
    initializers.add(...);
}

七、实际应用场景与扩展

场景一:自定义 DispatcherServlet 行为

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";
    }

    // 🔑 覆写创建方法,启用 DispatcherServlet 的额外特性
    @Override
    protected FrameworkServlet createDispatcherServlet(
            WebApplicationContext servletAppContext) {
        DispatcherServlet servlet = new DispatcherServlet(servletAppContext);
        // 找不到 Handler 时抛出异常(而不是返回 404),便于统一异常处理
        servlet.setThrowExceptionIfNoHandlerFound(true);
        // 处理 OPTIONS 请求
        servlet.setDispatchOptionsRequest(true);
        // 处理 TRACE 请求
        servlet.setDispatchTraceRequest(false);
        return servlet;
    }

    @Override
    protected boolean isAsyncSupported() {
        return false;  // 关闭异步(简单场景提高性能)
    }
}

场景二:注册额外的 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";
    }

    // 🔑 注册自定义 Filter(会被自动映射到 DispatcherServlet 上)
    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {
            new CharacterEncodingFilter("UTF-8"),
            new RequestTimingFilter()
        };
    }
}

// 字符编码 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() {}
}

// 请求耗时统计 Filter
public class RequestTimingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        long start = System.nanoTime();  // 纳秒精度
        try {
            chain.doFilter(req, resp);
        } finally {
            long elapsed = System.nanoTime() - start;
            System.out.printf("[TIMING] 请求 %s 耗时: %.2fms%n",
                    req.getRemoteAddr(), elapsed / 1_000_000.0);
        }
    }
    @Override public void init(FilterConfig config) {}
    @Override public void destroy() {}
}

场景三:多 DispatcherServlet(多上下文隔离)

DispatcherServlet 默认只有一个吗? 是的,AbstractAnnotationConfigDispatcherServletInitializerregisterDispatcherServlet() 中只注册一个 DispatcherServlet。99% 的场景这就够了——所有请求 / 都进同一个 DispatcherServlet,由不同的 @Controller 分发。

但在某些场景下,确实需要多个 DispatcherServlet 来隔离不同的业务模块:

// 注册多个 DispatcherServlet,处理不同的 URL 路径
// 每个 DispatcherServlet 有自己独立的 Servlet ApplicationContext
// 它们共享同一个 Root ApplicationContext(Service/DAO 层)

// 1. 管理后台
public class AdminServletInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };  // 共享的 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 接口
public class ApiServletInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };  // 共享的 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";
    }
}

此时的容器结构:

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

注意:多 DispatcherServlet 会带来额外的复杂度——每个 ServletContext 独立、异常处理各自独立、拦截器需要分别配置。在实际项目中,更常见的做法是通过单一 DispatcherServlet + 模块化 @Controller 分包来组织代码,而不是真正地去创建多个 DispatcherServlet。多 DispatcherServlet 主要用于极端隔离场景(如不同团队维护完全独立的 Web 模块、需要完全不同的 Spring MVC 配置等),大多数情况下微服务架构会是更好的选择。

Q:既然 99% 场景只有一个 DispatcherServlet,那我们能定制的东西是不是就只剩下路径匹配、Filter、Listener 了?

不完全对。路径匹配、Filter、Listener 只是 Servlet 注册层面(即 AbstractAnnotationConfigDispatcherServletInitializer 能覆写的几个方法)的定制项。DispatcherServlet 内部还有一套完整的可插拔策略模式架构,能定制的远不止这些。

定制层次拆分:

第一层:Servlet 注册层(本文档重点讲解的)
  → getServletMappings()        ← URL 路径匹配
  → getServletFilters()         ← Filter 注册(如编码、鉴权 Filter)
  → createDispatcherServlet()   ← DispatcherServlet 行为开关
       (throwExceptionIfNoHandlerFound / dispatchOptionsRequest 等)
  → isAsyncSupported()          ← 异步支持开关

第二层:Spring MVC 内部组件层(在 @Configuration 类中通过 WebMvcConfigurer 配置)
  → HandlerMapping              ← URL → Controller 方法的映射策略
  → HandlerAdapter              ← 如何调用不同类型的处理器
  → HandlerInterceptor          ← 前置/后置拦截(在 Spring 体系内,比 Filter 更灵活)
  → HandlerExceptionResolver    ← Controller 异常如何转换为 HTTP 响应
  → ViewResolver                ← 逻辑视图名 → 实际视图(JSP / Thymeleaf / 纯 JSON)
  → HttpMessageConverter        ← 请求体/响应体序列化(JSON ↔ Java 对象)
  → LocaleResolver              ← 国际化
  → MultipartResolver           ← 文件上传
  → CorsRegistry                ← 跨域配置
  → 等等……

两层的关系是:第一层决定 DispatcherServlet"怎么被注册进 Tomcat",第二层决定 DispatcherServlet"内部怎么处理请求"。日常开发中,真正频繁打交道的是第二层——比如添加拦截器做登录校验、配置 Jackson 序列化策略等。

至于 Socket 连接管理、HTTP 报文解析、编码格式转换等底层工作,这些由 Tomcat(Servlet 容器) 在请求到达 DispatcherServlet 之前就已经处理完毕——DispatcherServlet 拿到的已经是封装好的 HttpServletRequest / HttpServletResponse 对象,底层细节对开发者完全透明。

场景四:自定义排序控制

// 当有多个 WebApplicationInitializer 时,可以通过 @Order 控制执行顺序
@Order(2)  // 值越小越先执行
public class PriorityInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    // ... (较高的优先级,先注册)
}

@Order(Ordered.LOWEST_PRECEDENCE)
public class FallbackInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    // ... (最低优先级,最后注册)
}

// 不指定 @Order 时,默认顺序为 Ordered.LOWEST_PRECEDENCE

八、最佳实践与避坑指南

问题说明解决方案
重复注册同时使用 web.xmlWebApplicationInitializer二选一,推荐纯 Java 方式
//* 混淆/ 是默认 Servlet 映射;/* 会拦截 JSP使用 /,静态资源用 DefaultServletHttpRequestHandler
把教学 Demo 当生产模板教学代码为清晰性拆成多个 Initializer 类(如 HelloServletAppInitializerApiServletAppInitializer 各注册不同的 Servlet)实际项目中所有注册逻辑应合并在一个 Initializer 中,或用 Spring Boot 自动配置替代
多 DispatcherServlet 滥用不必要的多 DispatcherServlet 导致配置重复、异常处理分散99% 的场景一个 DispatcherServlet 足够,通过模块化 @Controller 分包组织代码即可
异步支持不匹配DispatcherServlet 开启异步但 Filter 不支持确保所有 Filter 也调用 setAsyncSupported(true)
Root Context 作用域Root Context 的 Bean 不能被 Servlet Context 覆盖Root 放全局 Bean(Service/DAO),Servlet 放 Web Bean(Controller)
启动性能@HandlesTypes 触发全 classpath 扫描使用 Spring Boot 的自动配置可大幅减少扫描范围
异常导致启动失败某个 Initializer 的 onStartup() 抛异常在每个初始器中做好 try-catch 和日志
Maven 打包遗漏 SPI 文件资源插件未包含 META-INF/services/确认 pom.xmlresources 配置包含该目录

实际项目 vs 教学 Demo 的差异

本教程中的 Demo 工程特意将不同知识点拆分为独立的类(HelloServletAppInitializerApiServletAppInitializerListenerDemoAppInitializer),这是为了教学清晰度——每个类专注演示一个知识点。实际项目中:

传统 Spring MVC 项目:一切注册逻辑合并在一个 MySpringMvcInitializer 中:

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 也在这一个类里注册
    @Override protected Filter[] getServletFilters() {
        return new Filter[]{new CharacterEncodingFilter("UTF-8"), new RequestTimingFilter()};
    }

    // Listener(ContextLoaderListener)由 Spring 框架自动处理,无需手动 addListener()
}

Spring Boot 项目:连上面这一个类都不需要写,DispatcherServletAutoConfiguration 全部自动搞定。这与教学 Demo 中"一个功能一个类"的做法完全不同——教学分开是为了讲清楚,实际合并是为了做简单


九、Spring Boot 中的对应机制

在 Spring Boot 中,开发者通常不需要编写 WebApplicationInitializer。Spring Boot 通过自动配置内嵌 Servlet 容器机制,将 DispatcherServlet 的注册完全自动化:

9.1 核心配置类

// 源码位置: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 与传统方式的对比

维度传统 Spring MVCSpring Boot
配置方式继承 AbstractAnnotationConfigDispatcherServletInitializer使用 application.properties + @SpringBootApplication
容器外置 Tomcat/Jetty,打 WAR 包内嵌 Tomcat/Jetty/Undertow,打 JAR 包
DispatcherServlet 创建通过 createDispatcherServlet() 手动创建自动通过 DispatcherServletAutoConfiguration 创建
自定义 DispatcherServlet覆写 createDispatcherServlet()实现 WebMvcRegistrations 接口或注入 DispatcherServletRegistrationBean
Servlet 获取servletContext.addServlet()DispatcherServletRegistrationBean (ServletContextInitializer)
启动触发外置容器扫描 SPI 文件ServletWebServerApplicationContext 刷新时自动触发

9.3 Spring Boot 自定义示例

// 方式一:实现 WebMvcRegistrations 接口(推荐)
@Configuration
public class CustomWebMvcConfig implements WebMvcRegistrations {

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

// 方式二:注入 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 自动配置加载机制

// Spring Boot 3.x: META-INF/spring/org.springframework.boot.autoconfigure.
//                  AutoConfiguration.imports
// 内容(节选):
// 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

十、Jakarta EE 迁移说明

项目Java EE 8(Spring 5.x)Jakarta EE 9+(Spring 6.x / Boot 3.x)
包名javax.servlet.*jakarta.servlet.*
SPI 文件路径META-INF/services/javax.servlet.ServletContainerInitializerMETA-INF/services/jakarta.servlet.ServletContainerInitializer
Servlet 容器Tomcat 9- / Jetty 9Tomcat 10+ / Jetty 11+
ApplicationContextAnnotationConfigWebApplicationContextAnnotationConfigWebApplicationContext(包路径不变)

迁移步骤(3 分钟完成):

  1. 全局搜索替换:javax.servletjakarta.servlet
  2. 确保 SPI 文件路径同步更新
  3. 升级到 Tomcat 10+ 或 Jetty 11+
  4. 重新编译验证

附录:SPI 文件与容器发现流程总结

┌──────────────────────────────────────────────┐
│       Servlet 3.0+ SPI 自动发现机制           │
├──────────────────────────────────────────────┤
│ 1. Jar 包中包含:                              │
│    META-INF/services/                          │
│    └── javax.servlet.ServletContainerInitializer│
│    内容: org.example.MyInitializer             │
│                                                │
│ 2. 容器(Tomcat)启动时:                     │
│    ├── 扫描所有 Jar 包                        │
│    ├── 读取 SPI 文件内容                      │
│    ├── 反射加载 MyInitializer                 │
│    ├── 读取 @HandlesTypes 注解                │
│    ├── 扫描 classpath 收集子类型              │
│    └── 调用 onStartup(classes, ctx)           │
│                                                │
│ 3. 在 onStartup() 中:                        │
│    ├── 遍历子类型列表                          │
│    ├── 实例化非接口/非抽象类                  │
│    └── 逐个调用 onStartup(ServletContext)     │
│                                                │
│ 4. 在 WebApplicationInitializer 中:          │
│    ├── 创建 ApplicationContext                │
│    ├── 创建 DispatcherServlet                 │
│    └── servletContext.addServlet() → 注册     │
└──────────────────────────────────────────────┘

关于文中"模拟"代码的说明:本文多处使用了简化版的模拟类(如 SpringServletContainerInitializer 的副本)来展示核心逻辑。这些代码是教学版本,旨在帮助理解设计思想而非替换源码阅读。真实 Spring 源码请查看 spring-web 模块:

  • 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 版本)