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

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:

java 复制代码
// 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 为例(配套源码):

java 复制代码
// 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 报文:

vbnet 复制代码
GET /api/info HTTP/1.1
Host: localhost:8080
Accept: application/json

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

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

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

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

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

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

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

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

维度 普通 Servlet DispatcherServlet
如何处理请求 你自己在 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 全程可插拔

简单类比:

java 复制代码
// 普通 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 中:

xml 复制代码
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <scope>provided</scope>  <!-- 关键:provided -->
</dependency>

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

其他 Servlet 容器也不止 Tomcat:

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

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


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

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

复制代码
浏览器 ──HTTP──▶ Tomcat ──Java对象──▶ 你的 Servlet
  ◀──HTTP──                    ◀──Java对象──

拆开来看完整流程:

css 复制代码
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 接口):

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

这样做有两个目的:

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

一张图总结整个数据流:

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

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

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

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

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

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

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

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

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

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

css 复制代码
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 类里 在你自己的启动类里
打包方式 war jar(Fat JAR,内嵌 Tomcat)

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

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

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

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

用图表示就是:

css 复制代码
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 规范定义,优先级从高到低):

vbnet 复制代码
请求: /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/info ApiServlet
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 为例:

java 复制代码
@Controller
public class HomeController {          // ← 没有 extends HttpServlet

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

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

实际的调用链是这样的:

vbnet 复制代码
请求 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

类比一下:

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

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

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

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

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

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

但关键区别在于:

维度 MySpringMvcInitializer HelloServlet/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() 编程式注册

简单类比:

java 复制代码
// 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"。实际发生的顺序是:

arduino 复制代码
① 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 的注册:

java 复制代码
// 源码出处: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 隔离):

java 复制代码
// 场景:一个 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(权限、日志、编码)
FORWARD request.getRequestDispatcher().forward() 防止 Forward 重复记录日志
INCLUDE request.getRequestDispatcher().include() JSP <jsp:include> 时的特殊处理
ERROR <error-page> 机制触发 错误页面的特殊处理
ASYNC 异步 Servlet 的 startAsync() 异步请求的上下文清理

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

java 复制代码
// 源码出处: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 链编排:

java 复制代码
// 场景:多个 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 接口 触发时机 典型用途
ServletContextListener contextInitialized() / contextDestroyed() 应用启动/关闭时初始化/销毁全局资源(DataSource、线程池)
ServletContextAttributeListener 属性增/删/改时 监控全局上下文属性的变更

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

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

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

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

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

java 复制代码
// 源码出处: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 组合应用:

java 复制代码
// 场景:一个完整的 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.Dynamic FilterRegistration.Dynamic void(无返回值)
配置内容 URL 映射、启动顺序、异步支持、初始化参数 URL 模式匹配、Servlet 名称匹配、请求类型 无(类型由接口决定)
执行时机 请求处理(doGet/doPost) 请求拦截(doFilter) 生命周期回调(初始化/销毁/创建/销毁)
典型应用 DispatcherServlet、REST API Servlet 编码、安全、日志、性能监控 全局资源初始化、在线统计、请求审计
多实例关系 独立处理请求,互不干扰 形成 FilterChain,顺序执行 各自监听不同事件,互不依赖
Spring 封装 DispatcherServletRegistrationBean FilterRegistrationBean ContextLoaderListener(ServletContextListener)

核心设计思想:

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

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


二、Servlet 3.0 的 SPI 约定详解

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

bash 复制代码
META-INF/services/javax.servlet.ServletContainerInitializer

2.1 核心接口

java 复制代码
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 注解的作用:

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

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

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

2.3 完整 SPI 演示代码

java 复制代码
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 源码分析

java 复制代码
// 源码位置: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 接口

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

四、继承链深度解析

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

scss 复制代码
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 逐层源码剖析

java 复制代码
// ============================================================
// 第一层: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 子类的实际用法

java 复制代码
// ============================================================
// 实际项目中的用法 ------ 只需继承并实现抽象方法
// ============================================================
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 之间存在父子关系:

less 复制代码
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 类来建立的:

java 复制代码
// 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 设计](#六、关键设计模式分析 → 6.2 父子 ApplicationContext 设计 "#62-%E7%88%B6%E5%AD%90-applicationcontext-%E8%AE%BE%E8%AE%A1")。


五、完整时序图

csharp 复制代码
  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 Context super.onStartup(servletContext)
创建 Servlet Context + 注册 DispatcherServlet registerDispatcherServlet()
编程式注入到 Servlet 容器 servletContext.addServlet(name, servlet)

六、关键设计模式分析

6.1 模板方法模式

scss 复制代码
AbstractContextLoaderInitializer.onStartup()
  └── createRootApplicationContext()        ← 抽象方法,子类实现

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

为什么用模板方法模式?

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

6.2 父子 ApplicationContext 设计

java 复制代码
// 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 中的三重过滤:

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

七、实际应用场景与扩展

场景一:自定义 DispatcherServlet 行为

java 复制代码
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

java 复制代码
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 来隔离不同的业务模块:

java 复制代码
// 注册多个 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";
    }
}

此时的容器结构:

javascript 复制代码
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 内部还有一套完整的可插拔策略模式架构,能定制的远不止这些。

定制层次拆分:

scss 复制代码
第一层: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 对象,底层细节对开发者完全透明。

场景四:自定义排序控制

java 复制代码
// 当有多个 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 中:

java 复制代码
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 核心配置类

java 复制代码
// 源码位置: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 MVC Spring 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 自定义示例

java 复制代码
// 方式一:实现 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 自动配置加载机制

java 复制代码
// 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.ServletContainerInitializer META-INF/services/jakarta.servlet.ServletContainerInitializer
Servlet 容器 Tomcat 9- / Jetty 9 Tomcat 10+ / Jetty 11+
ApplicationContext AnnotationConfigWebApplicationContext AnnotationConfigWebApplicationContext(包路径不变)

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

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

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

css 复制代码
┌──────────────────────────────────────────────┐
│       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 版本)
相关推荐
码事漫谈1 小时前
让 Codex CLI 跑上 DeepSeek:用 LiteLLM 做 Responses↔Chat 协议翻译
后端
JavaGuide1 小时前
又一个画图 Skill 开源,再见手动画 draw.io!
前端·后端·github
吾诺2 小时前
2026大直径紧固件双叠自锁垫圈怎么选型
后端
Jazz_z2 小时前
通过 Java 实现 Word 转 TXT
java·c#·word
tyung2 小时前
zhenyi-base zqueue 基准测试分享
后端·go
time展天12 小时前
Dddify:给 ASP.NET Core 项目一套轻量、清晰、可落地的 DDD 基础设施
数据库·后端·asp.net
SamDeepThinking2 小时前
为什么我会主动给一个年轻程序员加薪、晋升?
java·后端·程序员
tonydf2 小时前
接入服务商模式的支付宝支付
后端·支付宝
八苦2 小时前
MCP 企业托管授权(Enterprise-Managed Authorization)
后端