Servlet 3.0 怎么实现「无 web.xml」配置?@WebServlet 和 ServletContainerInitializer 是什么?
简化版
Servlet 3.0(2009 年)引入了「无 web.xml」的配置方式——用注解和编程式 API 替代传统的 web.xml 配置文件,让 Web 应用配置更简单、更模块化。核心两块:① 注解配置——@WebServlet("/hello")(注册 Servlet,替代 web.xml 的 <servlet> + <servlet-mapping>)、@WebFilter(Filter)、@WebListener(Listener),直接在类上标注就能注册;② 编程式 + SPI 机制(ServletContainerInitializer)——这是框架(如 Spring MVC)实现「零 web.xml 集成」的关键:容器启动时,会通过 SPI 机制(META-INF/services/javax.servlet.ServletContainerInitializer 文件)找到所有实现了 ServletContainerInitializer 的类,调用它们的 onStartup 方法,让框架在启动时编程式地注册自己的 Servlet/Filter(如 Spring 的 SpringServletContainerInitializer 会注册 DispatcherServlet)。配合 @HandlesTypes 注解,容器还能扫描出特定类型的类交给它处理。核心:Servlet 3.0 用注解(@WebServlet 等)和 SPI 机制(ServletContainerInitializer)替代 web.xml,让配置注解化、框架能零 web.xml 自动集成(Spring MVC 就靠这个)。
详细版
Servlet 3.0 的无 web.xml 方式:
| 方式 | 作用 | 替代 |
|---|---|---|
| @WebServlet | 注册 Servlet | <servlet> + <servlet-mapping> |
| @WebFilter | 注册 Filter | <filter> + <filter-mapping> |
| @WebListener | 注册 Listener | <listener> |
| ServletContainerInitializer | 启动时编程式注册(SPI) | 框架集成 |
| ServletContext.addServlet | 编程式注册 Servlet | 动态注册 |
// ① 注解注册 Servlet(替代 web.xml)
@WebServlet(urlPatterns = "/hello", loadOnStartup = 1)
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) { ... }
}
// ② ServletContainerInitializer(框架集成,SPI 机制)
@HandlesTypes(WebApplicationInitializer.class) // 容器扫描这些类型交给我
public class MyServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> classes, ServletContext ctx) {
// 容器启动时调用,编程式注册 Servlet/Filter
ServletRegistration.Dynamic servlet =
ctx.addServlet("dispatcher", new DispatcherServlet());
servlet.addMapping("/");
servlet.setLoadOnStartup(1);
}
}
// 注册(SPI):META-INF/services/javax.servlet.ServletContainerInitializer
// 内容一行:com.example.MyServletContainerInitializer
Spring MVC 的零 web.xml 集成(基于这个机制):
Spring 提供 SpringServletContainerInitializer(实现 ServletContainerInitializer)
→ 通过 SPI 被容器发现
→ @HandlesTypes(WebApplicationInitializer.class) 让容器找出所有
WebApplicationInitializer 实现类
→ 调用它们,让你注册 DispatcherServlet
→ 所以 Spring MVC 不用 web.xml(用 WebApplicationInitializer 配置)
⚠️ 理解 Servlet 3.0 的关键是「
ServletContainerInitializer+ SPI 机制」——这是框架(Spring MVC)实现「零 web.xml 自动集成」的底层原理。传统上,用 Spring MVC 要在 web.xml 里配DispatcherServlet;Servlet 3.0 后不用了——Spring 利用ServletContainerInitializer的 SPI 机制:Spring 的 jar 里有一个META-INF/services/javax.servlet.ServletContainerInitializer文件(SPI 声明),指向SpringServletContainerInitializer;Web 容器(Tomcat)启动时会自动扫描所有 jar 的这个 SPI 文件,找到并调用这些 initializer 的onStartup。Spring 的 initializer 配了@HandlesTypes(WebApplicationInitializer.class)——容器会扫描出所有实现WebApplicationInitializer的类,作为参数传给onStartup,Spring 再调用你写的WebApplicationInitializer来注册DispatcherServlet。所以「你写个WebApplicationInitializer就能配置 Spring MVC、完全不用 web.xml」,背后是这条「SPI → ServletContainerInitializer → @HandlesTypes 扫描 → 编程式注册」的链路。Spring Boot 的内嵌容器更进一步(连这个都自动化了)。
完整版教学
一、传统 web.xml 的问题
先理解为什么要「无 web.xml」:
传统 web.xml 配置(Servlet 3.0 之前):
所有 Servlet、Filter、Listener 都要在 web.xml 里配:
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>com.example.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
问题:
① 繁琐——每个 Servlet 要写一大段 XML(配置 + 映射)
② 集中——所有配置堆在一个 web.xml,大了难维护
③ 框架集成麻烦——用 Spring MVC 要在 web.xml 配 DispatcherServlet
④ 不模块化——第三方库要用户手动在 web.xml 加配置
Servlet 3.0 的改进(无 web.xml):
① 注解——@WebServlet 直接在类上注册(不用 XML)
② 编程式 + SPI——框架能"自己"注册(不用用户配 web.xml)
→ 配置注解化、模块化、框架零 web.xml 集成
所以 Servlet 3.0 无 web.xml = 用注解和 SPI 替代繁琐的 XML 配置
传统 web.xml 的问题——所有 Servlet/Filter/Listener 都要在 web.xml 配(繁琐、集中难维护、框架集成麻烦、不模块化)。Servlet 3.0 改进(无 web.xml):① 注解(@WebServlet 直接在类上注册)、② 编程式+SPI(框架能自己注册不用用户配 web.xml)——配置注解化、模块化、框架零 web.xml 集成。理解「传统 web.xml 问题:繁琐/集中难维护/框架集成麻烦/不模块化;Servlet 3.0 无 web.xml:注解(@WebServlet 类上注册)+编程式 SPI(框架自己注册);配置注解化模块化框架零集成」,就理解了无 web.xml 的动机。
二、注解配置
Servlet 3.0 的第一块是「注解配置」:
注解直接在类上注册组件(替代 web.xml):
@WebServlet(注册 Servlet):
@WebServlet(urlPatterns = "/hello", loadOnStartup = 1)
public class HelloServlet extends HttpServlet { ... }
→ 替代 web.xml 的 <servlet> + <servlet-mapping>
参数:urlPatterns(映射路径)、loadOnStartup(启动时加载)、
initParams(初始化参数)、asyncSupported(异步支持)
@WebFilter(注册 Filter):
@WebFilter("/*")
public class MyFilter implements Filter { ... }
→ 替代 <filter> + <filter-mapping>
@WebListener(注册 Listener):
@WebListener
public class MyListener implements ServletContextListener { ... }
→ 替代 <listener>
好处:
① 简单——直接在类上标注,不用写 XML
② 就近——配置和类在一起(一看就知道这个 Servlet 映射到哪)
③ 模块化——每个组件自带配置
容器怎么发现这些注解:
容器启动时扫描 classpath 的类,找带 @WebServlet 等注解的
→ 自动注册(不用 web.xml)
(可用 web.xml 的 metadata-complete=true 关闭扫描)
所以注解配置 = @WebServlet/@WebFilter/@WebListener 直接注册组件
Servlet 3.0 的注解配置——直接在类上注册组件:@WebServlet(urlPatterns, loadOnStartup)(注册 Servlet,替代 <servlet>+<servlet-mapping>)、@WebFilter(Filter)、@WebListener(Listener)。好处:简单(不用 XML)、就近(配置和类在一起)、模块化(每个组件自带配置)。容器启动时扫描 classpath 找带这些注解的类自动注册(可用 metadata-complete=true 关闭扫描)。理解「注解配置:@WebServlet(路径/loadOnStartup)注册 Servlet 替代 web.xml、@WebFilter/@WebListener;好处简单+就近+模块化;容器启动扫描 classpath 找注解自动注册」,就掌握了注解配置。
三、编程式注册
Servlet 3.0 还支持「编程式注册」——用代码注册:
编程式注册(ServletContext 的方法):
ServletContext.addServlet(name, servlet/class):动态注册 Servlet
ServletContext.addFilter(...):注册 Filter
ServletContext.addListener(...):注册 Listener
例:
ServletRegistration.Dynamic servlet =
ctx.addServlet("hello", HelloServlet.class);
servlet.addMapping("/hello");
servlet.setLoadOnStartup(1);
在哪调用(时机):
① ServletContextListener 的 contextInitialized(应用启动时)
② ServletContainerInitializer 的 onStartup(更早,框架用,见下节)
为什么要编程式:
① 动态——根据条件决定注册什么(注解是静态的)
② 框架集成——框架在启动时编程式注册自己的 Servlet
(框架不知道用户的配置,要动态注册)
编程式 vs 注解:
注解:静态、简单(固定的 Servlet 配置)
编程式:动态、灵活(条件注册、框架用)
所以编程式注册 = 用 addServlet 等 API 在启动时动态注册组件
Servlet 3.0 的编程式注册——用代码注册(ServletContext.addServlet/addFilter/addListener)。例:ctx.addServlet("hello", HelloServlet.class) + addMapping + setLoadOnStartup。在哪调用:ServletContextListener.contextInitialized(应用启动时)或 ServletContainerInitializer.onStartup(更早、框架用)。为什么要编程式:① 动态(根据条件决定注册什么、注解是静态的)、② 框架集成(框架启动时编程式注册自己的 Servlet)。理解「编程式注册:addServlet/addFilter/addListener 代码注册、在 ServletContextListener 或 ServletContainerInitializer 调用;为什么:动态(条件注册)+框架集成(框架启动时注册);vs 注解:注解静态简单、编程式动态灵活框架用」,就掌握了编程式注册。
四、ServletContainerInitializer 与 SPI
ServletContainerInitializer + SPI 是框架零 web.xml 集成的关键:
ServletContainerInitializer(SCI):
一个接口,容器启动时会调用它的 onStartup
→ 让框架/库在容器启动时"编程式注册自己的组件"
SPI 机制(怎么被容器发现):
① 框架的 jar 里有一个 SPI 声明文件:
META-INF/services/javax.servlet.ServletContainerInitializer
内容:实现类的全限定名(如 com.example.MySCI)
② 容器(Tomcat)启动时:
扫描所有 jar 的这个 SPI 文件
找到所有 ServletContainerInitializer 实现类
调用它们的 onStartup
→ 框架不用用户配 web.xml,自己通过 SPI 被发现、注册
@HandlesTypes(配合 SCI):
@HandlesTypes(WebApplicationInitializer.class)
→ 让容器"扫描出所有实现/继承指定类型的类"
→ 作为参数传给 onStartup(Set<Class<?>> classes, ...)
→ SCI 可以处理这些类(如调用它们)
完整机制:
容器启动 → SPI 找到 SCI → @HandlesTypes 扫描出相关类
→ 调 SCI.onStartup(相关类, ctx) → SCI 处理(注册组件)
所以 SCI + SPI + @HandlesTypes = 框架零 web.xml 自动集成的底层
ServletContainerInitializer(SCI)+ SPI 是框架零 web.xml 集成的关键:SCI 是一个接口,容器启动时调 onStartup(让框架编程式注册组件)。SPI 机制(怎么被发现):框架 jar 里有 SPI 声明文件(META-INF/services/javax.servlet.ServletContainerInitializer 内容是实现类全限定名),容器启动时扫描所有 jar 的这个 SPI 文件、找到所有 SCI 实现类、调 onStartup(框架不用用户配 web.xml、自己通过 SPI 被发现)。@HandlesTypes:让容器扫描出所有实现指定类型的类作为参数传给 onStartup。完整机制:容器启动→SPI 找到 SCI→@HandlesTypes 扫描相关类→调 onStartup 处理。理解「ServletContainerInitializer(SCI):容器启动调 onStartup 让框架注册组件;SPI 机制:框架 jar 有 META-INF/services 声明文件、容器启动扫描所有 jar 找 SCI 调 onStartup;@HandlesTypes 让容器扫描指定类型类传给 onStartup;SCI+SPI+@HandlesTypes 是框架零 web.xml 集成底层」,就掌握了 SCI 与 SPI。
五、Spring MVC 的零 web.xml 集成
看 Spring MVC 怎么用这个机制实现零 web.xml:
Spring MVC 的零 web.xml 集成(基于 SCI + SPI):
① Spring 的 spring-web jar 里有:
META-INF/services/javax.servlet.ServletContainerInitializer
内容:org.springframework.web.SpringServletContainerInitializer
② SpringServletContainerInitializer(Spring 的 SCI):
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer
implements ServletContainerInitializer {
public void onStartup(Set<Class<?>> classes, ServletContext ctx) {
// classes 是所有 WebApplicationInitializer 实现类(容器扫描出的)
// 遍历、实例化、调用它们的 onStartup
for (类 : classes) {
WebApplicationInitializer initializer = 实例化(类);
initializer.onStartup(ctx);
}
}
}
③ 你写一个 WebApplicationInitializer(配置 Spring MVC):
public class MyWebInit implements WebApplicationInitializer {
public void onStartup(ServletContext ctx) {
// 注册 DispatcherServlet
AnnotationConfigWebApplicationContext appCtx = ...;
DispatcherServlet servlet = new DispatcherServlet(appCtx);
ServletRegistration.Dynamic reg = ctx.addServlet("dispatcher", servlet);
reg.addMapping("/");
reg.setLoadOnStartup(1);
}
}
完整链路:
容器启动 → SPI 找到 SpringServletContainerInitializer
→ @HandlesTypes 扫描出所有 WebApplicationInitializer
→ 调 SpringSCI.onStartup(这些类, ctx)
→ SpringSCI 调你的 WebApplicationInitializer.onStartup
→ 你注册 DispatcherServlet
→ 完全不用 web.xml!
所以 Spring MVC 靠 SCI + SPI + WebApplicationInitializer 零 web.xml 集成
Spring MVC 零 web.xml 集成(基于 SCI+SPI):① Spring jar 里有 SPI 声明(指向 SpringServletContainerInitializer)→ ② SpringServletContainerInitializer(Spring 的 SCI,@HandlesTypes(WebApplicationInitializer.class),onStartup 遍历实例化调用所有 WebApplicationInitializer)→ ③ 你写一个 WebApplicationInitializer 注册 DispatcherServlet。完整链路:容器启动→SPI 找到 SpringSCI→@HandlesTypes 扫描出所有 WebApplicationInitializer→调 SpringSCI.onStartup→调你的 WebApplicationInitializer→注册 DispatcherServlet(完全不用 web.xml)。理解「Spring MVC 零 web.xml:Spring jar SPI 声明→SpringServletContainerInitializer(@HandlesTypes WebApplicationInitializer,遍历调用)→你写 WebApplicationInitializer 注册 DispatcherServlet;链路:容器启动→SPI 找 SpringSCI→扫描 WebApplicationInitializer→调用→注册 DispatcherServlet」,就理解了 Spring MVC 的零 web.xml 集成。
六、总结与演进
总结 Servlet 3.0 的无 web.xml 和演进:
Servlet 3.0 无 web.xml 的两块:
① 注解配置(用户用):
@WebServlet/@WebFilter/@WebListener 直接在类上注册
→ 简单场景,用户注解注册组件
② SCI + SPI(框架用):
ServletContainerInitializer + SPI + @HandlesTypes
→ 框架零 web.xml 自动集成(Spring MVC)
演进到 Spring Boot:
Spring Boot 用内嵌容器(Tomcat 内嵌在应用里)
→ 连 SCI 都不用了,直接编程式启动嵌入的 Tomcat + 注册 DispatcherServlet
→ @SpringBootApplication 一个注解,自动配置 Web
→ 更彻底的"无 web.xml"(连 SCI/WebApplicationInitializer 都不用写)
对比:
传统:web.xml 配置 DispatcherServlet
Servlet 3.0:WebApplicationInitializer(SCI + SPI)
Spring Boot:@SpringBootApplication(内嵌容器,全自动)
理解意义:
Servlet 3.0 的 SCI + SPI 是"框架自动集成"的基础
Spring Boot 的自动配置更进一步(但底层思想一脉相承)
核心总结:
Servlet 3.0 无 web.xml:注解(@WebServlet 等)+ SCI/SPI(框架集成)
框架(Spring MVC)靠 SCI + SPI + @HandlesTypes 零 web.xml 自动注册
Spring Boot 内嵌容器更彻底
Servlet 3.0 无 web.xml 两块:① 注解配置(用户用,@WebServlet 等直接注册)、② SCI+SPI(框架用,ServletContainerInitializer+SPI+@HandlesTypes 零 web.xml 自动集成,Spring MVC)。演进到 Spring Boot(内嵌容器、连 SCI 都不用、@SpringBootApplication 全自动、更彻底无 web.xml)。对比:传统 web.xml → Servlet 3.0 WebApplicationInitializer(SCI+SPI)→ Spring Boot @SpringBootApplication(内嵌容器全自动)。理解「Servlet 3.0 无 web.xml 两块:注解(@WebServlet 用户用)+SCI/SPI(框架集成);演进 Spring Boot 内嵌容器更彻底(@SpringBootApplication 全自动);SCI+SPI 是框架自动集成基础」,就掌握了总结与演进。
记忆钩子:「Servlet 3.0 无 web.xml 两块:①注解配置(用户用):@WebServlet(路径/loadOnStartup)注册 Servlet/@WebFilter/@WebListener 直接类上标注替代 web.xml,容器启动扫描 classpath 自动注册②SCI+SPI(框架用,★框架零 web.xml 集成关键):ServletContainerInitializer 容器启动调 onStartup 编程式注册组件,SPI 机制(框架 jar 有 META-INF/services/javax.servlet.ServletContainerInitializer 声明文件、容器启动扫描所有 jar 找 SCI 调用),@HandlesTypes 让容器扫描指定类型类传给 onStartup;★Spring MVC 零 web.xml:Spring jar SPI 声明→SpringServletContainerInitializer(@HandlesTypes WebApplicationInitializer)→你写 WebApplicationInitializer 注册 DispatcherServlet;Spring Boot 内嵌容器更彻底(@SpringBootApplication 全自动)」。
七、常见误区与追问
- 误区:Servlet 3.0 只是加了 @WebServlet 注解。 有两块:① 注解配置(@WebServlet/@WebFilter/@WebListener,用户直接在类上注册);② 更重要的是 ServletContainerInitializer + SPI 机制(框架零 web.xml 自动集成的底层,Spring MVC 就靠这个),后者才是框架能「不用 web.xml」的关键。
- 误区:Spring MVC 不用 web.xml 是因为 @WebServlet。 不是——Spring MVC 靠 ServletContainerInitializer + SPI + @HandlesTypes:Spring 的 jar 里有 SPI 声明文件指向 SpringServletContainerInitializer,容器启动时通过 SPI 发现并调用它,它扫描出所有 WebApplicationInitializer 实现类并调用,让你注册 DispatcherServlet;不是靠 @WebServlet 注解。
- 误区:ServletContainerInitializer 要在 web.xml 里配置才生效。 恰恰相反——它靠 SPI 机制自动被发现:框架的 jar 里有 META-INF/services/javax.servlet.ServletContainerInitializer 文件(SPI 声明),容器(Tomcat)启动时自动扫描所有 jar 的这个文件、找到并调用;完全不用 web.xml,这正是它能实现「零 web.xml 集成」的原因。
- 误区:@HandlesTypes 是可有可无的。 @HandlesTypes 是 ServletContainerInitializer 的重要配合——它让容器在调用 onStartup 之前,先扫描出所有实现/继承指定类型的类,作为参数传给 onStartup;Spring 用 @HandlesTypes(WebApplicationInitializer.class) 让容器找出所有 WebApplicationInitializer 实现类,这样 Spring 的 SCI 才能拿到并调用它们;没有它,SCI 就不知道有哪些用户的配置类。
- 追问:Servlet 3.0 怎么实现无 web.xml 配置? 两种方式:① 注解配置——用 @WebServlet(注册 Servlet,替代 web.xml 的 servlet + servlet-mapping)、@WebFilter、@WebListener 直接在类上标注,容器启动时扫描 classpath 自动注册;② 编程式 + SPI 机制——通过 ServletContainerInitializer(容器启动时调 onStartup 编程式注册组件),靠 SPI 机制(META-INF/services 声明文件)被容器自动发现,配合 @HandlesTypes 让容器扫描特定类型的类,这是框架(Spring MVC)实现零 web.xml 自动集成的关键。
- 追问:Spring MVC 是怎么做到不用 web.xml 配置 DispatcherServlet 的? 靠 ServletContainerInitializer + SPI + @HandlesTypes 的链路:① Spring 的 jar 里有 META-INF/services/javax.servlet.ServletContainerInitializer 文件(SPI 声明),指向 SpringServletContainerInitializer;② 容器(Tomcat)启动时通过 SPI 自动发现并调用 SpringServletContainerInitializer 的 onStartup;③ 它用 @HandlesTypes(WebApplicationInitializer.class) 让容器扫描出所有 WebApplicationInitializer 实现类,传给 onStartup;④ 它遍历、实例化、调用这些 WebApplicationInitializer 的 onStartup,让你在里面编程式注册 DispatcherServlet;所以你写一个 WebApplicationInitializer 就能配置 Spring MVC,完全不用 web.xml。
- 追问:Servlet 3.0 的这套机制和 Spring Boot 什么关系? Spring Boot 更进一步——它用内嵌容器(把 Tomcat 内嵌在应用里,不是部署到外部容器),直接编程式启动内嵌 Tomcat 并注册 DispatcherServlet,连 ServletContainerInitializer/WebApplicationInitializer 都不用写;@SpringBootApplication 一个注解就自动配置好 Web;但底层思想一脉相承(都是编程式注册、自动集成),Servlet 3.0 的 SCI + SPI 是「框架自动集成」的基础,Spring Boot 的自动配置是它的延续和简化。
八、加强记忆
Servlet 3.0(2009)引入「无 web.xml」配置——用注解和编程式 API 替代 web.xml。两块:① 注解配置(用户用)——@WebServlet("/hello")(注册 Servlet,替代 <servlet>+<servlet-mapping>)、@WebFilter(Filter)、@WebListener(Listener),容器启动扫描 classpath 自动注册;② ServletContainerInitializer(SCI)+ SPI 机制(框架用,零 web.xml 集成的关键)——SCI 是接口,容器启动时调 onStartup 让框架编程式注册组件;靠 SPI 机制被发现(框架 jar 里有 META-INF/services/javax.servlet.ServletContainerInitializer 声明文件,容器启动时扫描所有 jar 的这个文件、找到并调用 SCI);@HandlesTypes 让容器扫描出指定类型的类传给 onStartup。Spring MVC 的零 web.xml 集成就靠这个:Spring jar 的 SPI 声明 → SpringServletContainerInitializer(@HandlesTypes(WebApplicationInitializer.class))→ 扫描出所有 WebApplicationInitializer → 调用它们 → 你注册 DispatcherServlet(完全不用 web.xml)。Spring Boot 内嵌容器更彻底(连 SCI 都不用、@SpringBootApplication 全自动)。一句话「Servlet 3.0 无 web.xml:注解(@WebServlet/@WebFilter/@WebListener 类上注册,容器扫描自动注册)+ServletContainerInitializer(SCI,容器启动调 onStartup 编程式注册,靠 SPI 声明文件被发现,@HandlesTypes 扫描指定类型类);★Spring MVC 零 web.xml 靠 SpringServletContainerInitializer(SPI)+@HandlesTypes(WebApplicationInitializer)→注册 DispatcherServlet;Spring Boot 内嵌容器全自动更彻底」。