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.ServletContainerInitializerSPI 机制 - 容器启动时自动扫描 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 对它的一个具体实现。 用继承链来看:
scssServlet (接口 --- 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.Servlet、javax.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)
(只暴露接口方法)
这样做有两个目的:
- 安全 :你不能调用
Request里 Tomcat 的内部方法,比如request.getConnector().stop()把服务器关了 - 隔离:将来 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 |
|---|---|---|
| 继承链 | WebApplicationInitializer → AbstractContextLoaderInitializer → AbstractDispatcherServletInitializer → AbstractAnnotationConfigDispatcherServletInitializer |
直接实现 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()。
教学说明 :本工程的示例代码(
HelloServletAppInitializer、ApiServletAppInitializer、ListenerDemoAppInitializer)为了教学清晰度而拆成了多个独立的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 的能力,其核心价值在于:
- 精确控制拦截范围 ------ 通过
addMappingForUrlPatterns()和addMappingForServletNames()两种方式 - 控制执行顺序 ------ 通过
setInitParameter()或EnumSet<DispatcherType>参数 - 完全替代
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排序,支持@Order和Ordered接口
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 默认只有一个吗? 是的,AbstractAnnotationConfigDispatcherServletInitializer 在 registerDispatcherServlet() 中只注册一个 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.xml 和 WebApplicationInitializer |
二选一,推荐纯 Java 方式 |
/ 与 /* 混淆 |
/ 是默认 Servlet 映射;/* 会拦截 JSP |
使用 /,静态资源用 DefaultServletHttpRequestHandler |
| 把教学 Demo 当生产模板 | 教学代码为清晰性拆成多个 Initializer 类(如 HelloServletAppInitializer、ApiServletAppInitializer 各注册不同的 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.xml 的 resources 配置包含该目录 |
实际项目 vs 教学 Demo 的差异
本教程中的 Demo 工程特意将不同知识点拆分为独立的类(HelloServletAppInitializer、ApiServletAppInitializer、ListenerDemoAppInitializer),这是为了教学清晰度------每个类专注演示一个知识点。实际项目中:
传统 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 分钟完成):
- 全局搜索替换:
javax.servlet→jakarta.servlet - 确保 SPI 文件路径同步更新
- 升级到 Tomcat 10+ 或 Jetty 11+
- 重新编译验证
附录: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.SpringServletContainerInitializerorg.springframework.web.WebApplicationInitializerorg.springframework.web.servlet.support.AbstractDispatcherServletInitializerorg.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializerorg.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration(Boot 版本)