Skip to content

S01-02 Servlet-基础 ​

[TOC]

概述 ​

Servlet介绍 ​

Servlet(Server Applet,服务端小程序) 是 Java Web 规范的核心标准之一,本质是运行在 Web 容器中的 Java 程序,负责接收 HTTP 请求并动态生成响应。

整个体系的学习分为两个阶段:

  • 第一阶段聚焦基础与核心通信(概念、生命周期、请求响应、上下文);
  • 第二阶段聚焦进阶架构与安全(会话跟踪、Filter 过滤器、Listener 监听器、异步与并发安全)。本篇首先展开第一阶段的核心机制。

诞生背景:

在早期的 Web 架构中,Web 服务器(如 Apache、Nginx)主要提供静态 HTML 页面、图片和文本文件的托管与传输。随着业务对动态内容交互(如用户认证、数据库读写、实时计算)的需求激增,传统的静态服务器无法满足业务需求:

  • CGI 的局限性:早期用于解决动态网页生成的技术是通用网关接口(CGI,Common Gateway Interface)。CGI 采用“进程复用”模式,每次客户端请求都会在操作系统中启动一个独立的进程。当并发量升高时,频繁创建和销毁进程会导致系统内存与 CPU 资源迅速耗尽。
  • Servlet 的提出:Sun 公司推出了基于 Java 平台的 Servlet 技术。Servlet 运行在单一的 Web 容器进程内部,采用多线程并发模型处理请求,大幅度降低了系统开销,并继承了 Java 语言“一次编写,到处运行”的跨平台与类型安全特性。

核心定位:

Servlet 在整个 Java Web 架构体系中处于承上启下的核心纽带位置:

  • 请求转发站:处于网络协议解析与业务服务层之间,屏蔽了底层复杂的 TCP 握手和 HTTP 报文解析细节。
  • 动态页面基石:后期的 JSP 技术、Struts 框架,以及现代主流的 Spring MVC(其核心 DispatcherServlet),本质上都是基于 Servlet 规范构建或演化而来的高级抽象。

容器角色:

Servlet 本身只是普通的 Java 类,内部并没有包含用于启动程序的 main() 方法,无法脱离环境独立运行。它必须部署在 Servlet 容器(也称 Web 容器,如 Apache Tomcat、Eclipse Jetty、Undertow)中:

  • 生命周期托管:容器通过反射机制负责 Servlet 的加载、实例化、初始化(init)、调用(service)以及销毁(destroy)。
  • 网络与协议封装:容器监听服务器端口,捕获 HTTP 请求报文并自动转换为规范的 HttpServletRequest 与 HttpServletResponse 抽象对象。
  • 线程池管理:容器维护并发工作线程池,将每个进入的 HTTP 请求指派给独立的线程执行,无需开发者手动创建和销毁底层线程。

体系结构 ​

Servlet 规范通过严格的分层面向对象设计构建了类继承体系:

  • jakarta.servlet.Servlet 顶层接口:规定了组件必须履行的生命周期契约,包含 init、service、destroy 以及元信息获取方法。

  • jakarta.servlet.GenericServlet 抽象类:提供了与底层协议无关的通用实现,封装了 Servlet 配置信息和日志功能,将 service 保留为抽象方法。

  • jakarta.servlet.http.HttpServlet 抽象类:专为 HTTP 协议设计的抽象类,覆写了 service 方法,并根据 HTTP 动词自动分发到 doGet、doPost、doPut、doDelete 等具体处理方法。

    java
    package com.example.servlet;
    
    import jakarta.servlet.http.HttpServlet;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    
    // 继承 HttpServlet 获得对 HTTP 请求与响应协议的完整支持
    public class OverviewServlet extends HttpServlet {
      // 覆写针对 GET 请求的专属分发逻辑
      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String action = req.getParameter("action");
      }
    }

工作流程 ​

从浏览器发起一次 HTTP 请求到最终呈现网页结果,Servlet 容器与 Servlet 协同完成以下核心步骤:

  1. 浏览器向服务端发送标准 HTTP 请求报文。

  2. 容器(如 Tomcat)监听端口接收到报文,解析请求行、请求头及请求体。

  3. 容器创建用于承载请求数据的 HttpServletRequest 对象与承载输出的 HttpServletResponse 对象。

  4. 容器根据请求的 URL 地址在内部路由表检索对应的 Servlet 类映射。

  5. 若目标 Servlet 尚未被加载,容器通过反射完成实例化并调用其 init() 方法完成初始化。

  6. 容器从线程池调取工作线程,调用 Servlet 的 service() 方法,并将请求分发至对应的 doGet 或 doPost 方法执行业务逻辑。

  7. Servlet 将处理结果写入响应对象,容器将响应对象重组为标准 HTTP 响应报文回传给浏览器。

  8. 容器释放或归还该次请求占用的工作线程。

image-20260904140149734

核心优势 ​

  • 高性能与高伸缩性:基于轻量级线程调度代替传统的进程创建,单台服务器的并发承载能力较 CGI 产生质的飞跃。
  • 跨平台与规范统一:遵循规范编写的 Servlet 可以无缝迁移运行在 Tomcat、Jetty、WebLogic 等任何兼容该规范的容器中。
  • 安全与健壮:拥有 Java 语言的强类型检查、自动垃圾回收(GC)以及严格的异常处理机制,杜绝了底层内存溢出引起的容器崩溃。
  • 功能扩展完备:具备完备的过滤器链(Filter)、监听器(Listener)以及异步非阻塞 I/O 支持,为现代大型微服务网关和 MVC 框架提供了扎实的底层协议处理支撑。

配置演进 ​

Servlet 的注册方式伴随规范版本的升级经历了演进:

  • XML 显式配置(Servlet 2.x):所有配置集中在 web.xml 文件中,通过 <servlet> 声明类名,通过 <servlet-mapping> 声明请求路由路径。配置与业务代码解耦,但随着工程规模增大,XML 文件极易变得冗长难以维护。
  • 注解驱动配置(Servlet 3.0+):引入 @WebServlet 注解,直接在类上方声明路由规则、初始化参数及启动顺序,大幅提升了开发效率与代码可读性。
  • 编程式注入(现代 Spring Boot):在嵌入式容器场景下,通过注册 ServletRegistrationBean 类或直接使用自动化装配机制,完全摆脱传统容器打包部署模式,实现轻量化运行。

Tomcat 与 Servlet ​

Tomcat 与 Servlet:Web 容器(如 Apache Tomcat)与 Servlet 之间是容器与组件的关系:

  • 协议解耦:Tomcat 负责底层的 Socket 监听、TCP 握手和 HTTP 协议报文解析。
  • 业务承载:Tomcat 将解析出的请求封装为标准对象,传递给 Servlet 执行业务逻辑,再将 Servlet 返回的响应写回客户端。
  • 继承层级:从底层接口到常用类依次为 Servlet 接口 →\rightarrow GenericServlet 抽象类 →\rightarrow HttpServlet 抽象类。日常开发直接继承 HttpServlet 即可。

基础 ​

开发入门 ​

在 Tomcat 9 环境下,基于纯手动依赖导入并实现最底层的 Servlet 接口,是理解 Web 容器运行本质与组件生命周期的最佳起点。

在开发前,必须明确版本契约与命名空间:

  • Tomcat 9 对应的规范:Tomcat 9 实现了 Servlet 4.0 规范,属于 Java EE 8 体系。
  • 包名空间:代码中引用的 Servlet 核心接口必须以 javax.servlet 开头。从 Tomcat 10 开始才迁移至 jakarta.servlet,两者混用会导致类找不到或容器无法识别。

步骤1:依赖导入 ​

手动构建项目依赖需要从本地解压版 Tomcat 9 中获取官方接口库:

  1. 前往 Apache Tomcat 官网下载 Tomcat 9 的二进制压缩包(如 apache-tomcat-9.0.xx.zip)并解压到无中文、无空格的本地目录。

  2. 进入 Tomcat 解压目录下的 lib 文件夹,复制核心文件 servlet-api.jar。该包仅包含规范接口定义,不包含具体的业务逻辑实现。

  3. 打开 IDEA,在创建好的 Web 项目的 WEB-INF 下创建 lib 目录,并复制 servlet-api.jar 包到该目录下。

  4. 右键 servlet-api.jar 包,选择 Add as Library 解压,此时项目中就可以引入该包了。

步骤2:编写代码 ​

直接实现 javax.servlet.Servlet 接口,需要完整重写 5 个基础生命周期契约方法。在 src 目录下新建 com.example.servlet.HelloServlet 类:

java
package com.example;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class HelloServlet implements Servlet {
  // 容器完成实例化后仅调用一次,用于初始化资源
  @Override
  public void init(ServletConfig config) {
    System.out.println("HelloServlet 初始化完成");
  }

  // 核心请求处理入口,每次客户端请求都会由容器线程执行
  @Override
  public void service(ServletRequest req, ServletResponse res) throws IOException {
    res.setContentType("text/html;charset=UTF-8");
    PrintWriter out = res.getWriter();
    out.println("<h1>Hello from Servlet!</h1>");
  }

  // 容器卸载应用或停止服务前调用,释放占用资源
  @Override
  public void destroy() {
    System.out.println("HelloServlet 已销毁");
  }

  // 获取当前 Servlet 的配置信息对象
  @Override
  public ServletConfig getServletConfig() {
    return null;
  }

  // 返回当前 Servlet 的作者、版本等元数据信息
  @Override
  public String getServletInfo() {
    return "HelloServlet/1.0";
  }
}

步骤3:路由配置 ​

Servlet 编写完成后,容器并不知道客户端访问哪个 URL 时该触发它,必须在 web/WEB-INF/web.xml 中完成请求路由与实现类的绑定:

xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
               http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
     version="4.0">

  <!-- 1. 声明 Servlet 组件 -->
  <servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.example.servlet.HelloServlet</servlet-class>
  </servlet>

  <!-- 2. 映射对外访问路径 -->
  <servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>

</web-app>

配置的关联逻辑如下:

  1. <url-pattern> 规定了外部请求的路径 /hello。

  2. 匹配路径后,Tomcat 查找与自身同名的 <servlet-name>。

  3. 通过 <servlet-class> 取得全限定类名,进而利用反射进行加载与调度。

步骤4:运行访问 ​

最后需要在 IDEA 中绑定本地 Tomcat 9 并部署运行:

  1. 点击 IDEA 顶部运行栏的 Current File 或 Add Configuration...。

  2. 点击左上角 + 号,向下滚动展开 Tomcat Server,选择 Local。

  3. 在 Server 选项卡中,点击 Application server 后方的 Configure...,将路径指向本地解压的 Tomcat 9 根目录。

  4. 切换到 Deployment 选项卡,点击 + 号,选择 Artifact,选择项目生成的 xxx:war exploded。

  5. 在下方 Application context 中设置应用的根路径(例如设置为 /servlet 或直接设置为 /)。

  6. 保存配置后,点击 IDEA 右上角的绿色三角形 Run 启动服务器。

  7. 控制台输出启动完成后,打开浏览器访问 http://localhost:8080/servlet/hello,页面正常显示 Hello Tomcat 9! 即代表部署成功。

API: Servlet ​

  • void init():(ServletConfig config),初始化实例。在 Servlet 实例化后由容器调用一次,用于完成资源加载、连接池分配和配置读取。

  • void service():(ServletRequest req, ServletResponse res),处理客户端请求。每次请求到来时由容器的线程池分发调用,负责核心业务逻辑流转。

  • void destroy():(),释放资源。在 Servlet 实例从容器中卸载或容器关机前调用,用于关闭文件流、后台线程和外部资源连接。

  • ServletConfig getServletConfig():(),获取配置对象。返回传递给 init() 方法的 ServletConfig 引用,用于读取初始化参数和全局 ServletContext 上下文。

  • String getServletInfo():(),获取元数据描述。返回关于 Servlet 的作者、版本、版权或自定义说明的纯文本字符串。

注意事项:

  1. 线程安全禁忌:容器在处理多个并发 HTTP 请求时,会复用同一个 Servlet 实例并在独立的线程中调用 service()。绝对禁止在 service() 中对未受保护的类成员字段执行写入操作,否则会直接引发并发写覆盖或可见性问题。
  2. init() 失败行为:若在 init() 执行期间抛出 ServletException 或 RuntimeException,容器会立即判定该 Servlet 实例化失败并不再路由任何请求,后续请求直接返回 500 或 404 状态码。
  3. destroy() 线程滞留排空:容器调用 destroy() 前,会尽量等待正在执行 service() 的线程执行完毕。但若业务逻辑中存在无限死循环或超长阻塞,容器等待超时后依然会强制注销,需在 destroy() 中提供中断指示位。
  4. 空指针异常风险:直接实现 javax.servlet.Servlet 接口时,getServletConfig() 必须开发者手动维护成员变量。若在 init(ServletConfig config) 中未显式执行 this.config = config;,调用该方法将返回 null,引发 NullPointerException(GenericServlet 已在此处做了内部封装)。
  5. 元数据输出限制:getServletInfo() 返回的字符串主要供 Web 容器管理控制台展示或监控日志采集,不得在其中包含密码、密钥等敏感配置信息。

示例代码见:编写代码

网络请求Servlet时序图 ​

单例机制与内存驻留 ​

Servlet 在 Web 容器中遵循单实例、多线程的设计架构:容器在整个生命周期内通常仅保留该 Servlet 的一个实例,常驻于堆内存中,并通过线程池复用工作线程并发调度该实例。

单例机制 ​

Servlet 的“单例”并非经典设计模式(GoF)中通过私有构造方法实现的单例,而是由 Web 容器生命周期管理的单实例:

  • 容器托管创建:Servlet 必须提供公共无参构造方法。Tomcat 在需要时利用反射技术创建实例,并将其托管在容器的上下文管理字典中。
  • 映射唯一性:默认情况下,每一个在配置(注解或部署描述符)中声明的 Servlet 映射,在同一个 Web 应用内只会被实例化一次。后续无论有多少客户端请求访问该 URL,Tomcat 都会调度同一个实例进行处理。

内存常驻 ​

在首次被加载并完成 init() 初始化后,Servlet 实例将常驻于 JVM 的堆内存中,其核心价值在于降低系统开销:

  • 避免瞬时分配:早期 CGI 协议每遇到一次请求便启动一个独立的系统进程,性能开销巨大;若每次 HTTP 请求都重新 new 一个 Servlet 实例,高并发下频繁的堆内存分配与垃圾回收(GC)也会拖垮应用。
  • 常驻与回收时机:只要宿主 Web 应用未被卸载,该实例会一直存活在堆中。只有当 Web 应用执行热卸载(Reload)、应用停止或 Tomcat 服务正常关闭时,Tomcat 才会触发其 destroy() 方法,之后该实例才会脱离 GC Roots 引用链并被垃圾回收器回收。

调度机制 ​

Tomcat 对 Servlet 的调用依托于内置的高性能线程池:

  1. 客户端发起 HTTP 并发请求到达服务器监听端口。

  2. Tomcat 的 Connector(连接器)组件接收 TCP 连接并解析底层 HTTP 报文。

  3. Tomcat 从自身维护的 Worker 工作线程池中提取空闲线程。

  4. 工作线程将报文组装为请求与响应对象,并发调用同一个 Servlet 实例的 service() 方法。

  5. service() 方法内部根据 HTTP 请求动词分发调用对应的 doGet() 或 doPost()。

  6. 请求处理完成并将结果写回客户端后,工作线程被归还给线程池,而 Servlet 实例依旧驻留内存准备迎接下一轮调用。

验证示例 ​

通过打印当前对象的内存哈希值以及执行线程名称,可以清晰验证单实例与多线程的调度过程:

java
package com.example;

import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class SingletonDemoServlet extends HttpServlet {
  private int requestCounter = 0;

  // 覆写 doGet 方法演示多线程并发调用同一实例
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // 共享成员变量在无并发控制下存在线程安全隐患
    requestCounter++;
    String threadName = Thread.currentThread().getName();
    int instanceHash = System.identityHashCode(this);
    resp.setContentType("text/plain;charset=UTF-8");
    resp.getWriter().printf("Thread: %s | Hash: %d | Count: %d%n", threadName, instanceHash, requestCounter);
  }
}

当多台客户端同时访问该接口时,控制台输出的 instanceHash 完全一致,而 Thread 名称各不相同,直接印证了多线程正在并发进入同一个对象实例。

生命周期 ​

Servlet 生命周期是指该对象从被 Web 容器加载、实例化,到提供服务处理请求,直至最终被销毁移出内存的完整全过程。

Servlet 的整个生命周期完全由 Web 容器(如 Apache Tomcat)全权掌控,开发者不需要也不应该通过 new 关键字手动创建或直接销毁它。

核心阶段:Servlet 的生命周期严格划分为四个核心节点:

  • 加载与实例化:容器通过反射机制定位并创建该 Servlet 的 Java 对象。
  • 初始化:容器调用 init() 方法,让对象准备好承接业务环境。
  • 服务调用:容器在接收到客户端请求时,指派线程调用 service() 方法处理请求。
  • 销毁:在应用卸载或服务关闭时,容器调用 destroy() 方法完成清理并释放资源。

阶段1:实例化 ​

实例化是 Servlet 生命周期的开端,决定了该对象何时出现在堆内存中:

  • 反射加载:Servlet 类必须具备公共无参构造函数。Tomcat 读取类全限定名后,通过 Java 反射完成类加载并生成实例。
  • 默认机制(懒加载):默认情况下,应用启动后并不会立即实例化 Servlet。只有当某个客户端第一次发送请求命中该 Servlet 的映射 URL 时,容器才会执行实例化。
  • 单例特性:在常规运行模式下,一个 Servlet 映射配置在同一个 Web 应用内只会被实例化一次,后续所有的请求都复用该实例。

阶段2:初始化 ​

对象实例化完毕后,容器会立刻触发初始化流程:

  • 方法调用:容器向对象传递封装了配置信息的 ServletConfig 参数,并调用 init(ServletConfig config) 方法。
  • 执行频次:在 Servlet 的整个生命周期中,init() 方法仅执行一次。
  • 核心职责:用于装配一次性系统资源,如读取初始化参数(init-param)、配置数据源连接、加载静态字典或预热内部缓存。
  • 异常阻断:若 init() 方法执行期间抛出 ServletException 或运行时异常,容器会认定该 Servlet 初始化失败,直接将其置为不可用状态,后续任何针对该路径的请求都将直接返回 500 错误。

阶段3:服务调用 ​

初始化成功后,Servlet 进入就绪状态,可以正式处理外部网络请求:

  • 线程调度:每次有客户端发起 HTTP 请求匹配该 Servlet 时,Tomcat 会从自身维护的 Worker 线程池中提取一个空闲线程来负责该次请求。
  • 方法分发:线程携带 HttpServletRequest 与 HttpServletResponse 两个参数调用 service() 方法。在 HttpServlet 体系中,service() 内部会进一步判断客户端使用的 HTTP 动作类型,自动分派给 doGet()、doPost()、doPut() 等方法。
  • 执行频次:随着客户端不断发起请求,服务方法在生命周期内会被调用多次。
  • 并发约束:由于多个工作线程会并发访问同一个 Servlet 实例,方法内部处理业务时必须避免依赖具有修改语义的类成员变量,保障线程安全。

阶段4:销毁 ​

当 Web 应用程序生命周期结束时,Servlet 实例会被移出堆内存:

  • 触发时机:Tomcat 服务正常停止、Web 容器重启(Redeploy),或者应用通过管理端执行热卸载(Context Reload)时触发。
  • 执行频次:在 Servlet 销毁之前由容器主动调用,仅执行一次。
  • 核心职责:用于执行非托管系统资源的清理收尾工作,包括注销后台轮询线程、关闭数据库连接池、释放文件输入输出流等。
  • 垃圾回收:destroy() 方法执行完毕后,Tomcat 释放对该 Servlet 实例的所有外部强引用。该对象进入可回收状态,最终由 JVM 的垃圾收集器(GC)清理出堆内存。

预加载机制 ​

为了解决默认“懒加载”导致的首次请求响应延迟问题,Servlet 规范提供了预加载配置选项:

  • 配置方式:在 web.xml 配置中使用 <load-on-startup> 标签,或在注解中使用 @WebServlet(loadOnStartup = n)。

  • 数值规则:

    • 若取值为负整数(如默认值 -1),容器保持懒加载策略,即首次请求时才实例化。
    • 若取值为 0 或正整数(如 1、2),容器将在应用启动部署阶段立即完成实例化并调用 init()。
    • 数值越小,代表加载初始化的优先级越高;数值相同的组件加载顺序由容器自行决定。
  • 典型场景:底层核心调度器(例如 Spring MVC 的 DispatcherServlet)通常配置为 loadOnStartup = 1,保证在系统正式对外开放端口前就完成全部 Controller 和 Bean 的扫描与挂载。

xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>

  <!-- 1. 声明 Servlet 组件 -->
  <servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.example.servlet.HelloServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <!-- 2. 映射对外访问路径 -->
  <servlet-mapping>...</servlet-mapping>

</web-app>

执行时序 ​

一个标准的 Servlet 从请求到达至最终销毁的完整物理时序如下:

  1. 客户端向服务器发送 HTTP 请求报文。

  2. Tomcat 监听端口捕获报文,解析请求 URL 并匹配对应的 Servlet 路由映射。

  3. Tomcat 检查内部注册表,判断该 Servlet 实例是否已经存在。

  4. 若实例不存在,容器调用无参构造函数完成实例化。

  5. 实例创建后,容器生成 ServletConfig 对象并调用 init() 完成初始化。

  6. 容器从线程池指派一个独立工作线程,调用 service() 并分派到具体的 doGet() 或 doPost()。

  7. 后续若有新请求到达,直接复用该驻留内存的实例,由新线程调用 service()。

  8. 容器关闭或应用卸载,容器调用 destroy() 释放资源,最后实例被 JVM 垃圾回收。

代码验证 ​

通过打印控制台日志,可以清晰观测到生命周期各阶段方法的执行顺序与触发次数:

java
package com.example;

import jakarta.servlet.ServletConfig;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class LifeCycleServlet extends HttpServlet {
  // 构造器:由容器反射调用创建对象实例
  public LifeCycleServlet() {
    System.out.println("1. Constructor executed");
  }

  // 初始化方法:容器完成实例化后调用且仅执行一次
  @Override
  public void init(ServletConfig config) {
    System.out.println("2. init executed");
  }

  // 业务响应方法:每次请求到达时由工作线程调用
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    System.out.println("3. doGet executed");
  }

  // 销毁方法:容器卸载应用或关闭前调用以释放资源
  @Override
  public void destroy() {
    System.out.println("4. destroy executed");
  }
}

连续发起多次访问并最终关闭 Tomcat,控制台输出的调用特征为:

  • 构造函数只打印 1 次;
  • init 方法只打印 1 次;
  • doGet 方法根据请求次数打印多次;
  • 关闭服务器时,destroy 方法只打印 1 次。

GET与POST请求分发 ​

在底层的 Servlet 规范中,所有网络请求的处理中枢都是通用接口方法 service(ServletRequest, ServletResponse)。针对 HTTP 协议的 GET 和 POST 请求分发,本质是在该方法内部对通用参数进行协议类型转换并根据请求方法分流。

分发机制

顶层接口 javax.servlet.Servlet 并不针对特定网络协议设计,其核心方法入参为通用的 ServletRequest 与 ServletResponse:

  • 协议解耦:ServletRequest 接口仅包含协议无关的基础通信规范,并没有直接定义 getMethod() 等 HTTP 协议专有的属性与方法。
  • 向下转型:当应用部署在 Web 容器(如 Tomcat)并接收 HTTP 请求时,容器传入的实际运行时实例是其 HTTP 子接口实现类。开发者必须将通用参数强制转换为 HttpServletRequest 与 HttpServletResponse。
  • 方法提取与路由:转型后即可调用 httpReq.getMethod() 提取 HTTP 动词,通过条件分支将控制权移交给 GET 或 POST 业务处理块。这就是 HttpServlet 抽象类底层封装的分发原理。

代码实现

以下代码基于最底层的 implements Servlet 规范,在接收通用 ServletRequest 和 ServletResponse 参数的前提下完成 GET 与 POST 的分发处理:

java
package com.example;

import jakarta.servlet.Servlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class DispatchDemoServlet implements Servlet {
  private ServletConfig config;

  @Override
  public void init(ServletConfig config) {
    this.config = config;
  }

  // 核心服务入口:接收通用协议请求与响应对象
  @Override
  public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    // 1. 将通用对象向下转型为 HTTP 协议专用对象以提取动词
    HttpServletRequest httpReq = (HttpServletRequest) req;
    HttpServletResponse httpRes = (HttpServletResponse) res;
    String method = httpReq.getMethod();

    // 2. 根据请求动词分发执行对应的处理逻辑
    if ("GET".equalsIgnoreCase(method)) {
      httpRes.getWriter().write("Handled GET");
    } else if ("POST".equalsIgnoreCase(method)) {
      httpRes.getWriter().write("Handled POST");
    }
  }

  @Override
  public void destroy() {}

  @Override
  public ServletConfig getServletConfig() {
    return this.config;
  }

  @Override
  public String getServletInfo() {
    return "DispatchDemo";
  }
}

HttpServlet ​

概述 ​

javax.servlet.http.HttpServlet 是 JavaWeb 开发中使用频率最高的抽象基类。它专门针对 HTTP 协议进行了全面封装,将底层繁琐的协议解析和方法分发抽象为标准生命周期方法,使开发者能够专注于具体的业务逻辑。

类定位:HttpServlet 在架构中扮演着“HTTP 协议适配器”的角色:

  • 专用协议支持:顶层的 Servlet 接口和 GenericServlet 抽象类均未绑定具体的网络应用层协议,而 HttpServlet 专为 HTTP 协议定制。
  • 业务开发基石:在日常开发中,自定义的 Servlet 绝大多数直接继承 HttpServlet,而无需从零实现基础接口。
  • 抽象类特征:HttpServlet 被声明为 abstract,但它没有包含任何抽象方法。这种设计是为了强制开发者通过继承来重写特定动词方法,防止直接将其本身实例化。

继承结构:HttpServlet 处在标准 Servlet 继承树的核心层级:

  • Servlet 接口:定义了最顶层的 5 个生命周期与元数据规范。
  • GenericServlet 抽象类:实现了 Servlet 接口,默认封装了初始化参数读取与日志记录,将 service() 留给子类实现。
  • HttpServlet 抽象类:继承 GenericServlet,重写了 service() 方法,实现了基于 HTTP 动词的分发逻辑。
  • 自定义 Servlet:开发者编写的业务类,通过继承 HttpServlet 并覆写 doGet、doPost 等方法提供具体功能。

动词方法 ​

HttpServlet 为常见 HTTP 规范动作提供了对应的处理方法,默认行为及含义如下:

方法名称对应 HTTP 动作幂等性默认缺省行为
doGetGET(获取资源)是返回 HTTP 405 Method Not Allowed
doPostPOST(提交数据)否返回 HTTP 405 Method Not Allowed
doPutPUT(更新/替换资源)是返回 HTTP 405 Method Not Allowed
doDeleteDELETE(删除资源)是返回 HTTP 405 Method Not Allowed
doHeadHEAD(仅获取响应头)是复用 doGet 输出,但拦截并丢弃响应体数据
doOptionsOPTIONS(协商支持能力)是自动扫描当前类重写的方法并填充 Allow 响应头
doTraceTRACE(回显诊断调试)是返回客户端请求报文副本,存在 XST 漏洞风险

幂等性(Idempotence) 是计算机科学与分布式系统中的一个核心概念。其核心定义为:同一个操作,无论是执行一次还是连续执行多次,对系统产生的最终状态或业务副作用都是相同的。

如果子类未覆写某个动词方法,而客户端以该动词发起访问,HttpServlet 默认会返回 405 状态码,明确告知客户端当前资源不支持该请求动词。

分发机制 ​

HttpServlet 内部最关键的实现是重载了两个 service 方法,构成了两级分发中枢:

  • 第一级:公共协议适配

    覆写来自父类的 public void service(ServletRequest req, ServletResponse res)。该方法首先判断参数是否为 HTTP 类型,若符合则向下转型为 HttpServletRequest 与 HttpServletResponse,并调用受保护的重载版本。

  • 第二级:动词路由分发

    内部受保护方法 protected void service(HttpServletRequest req, HttpServletResponse resp) 负责核心路由。它通过 req.getMethod() 提取 HTTP 动作名称(如 GET、POST),并通过条件分支分别调用对应的 doXxx 方法。

执行流程 ​

从外部网络发起一次 HTTP 请求到最终执行开发者编写的代码,完整调用流程如下:

  1. 浏览器向服务器指定路径发起包含特定动词的 HTTP 请求。

  2. Tomcat 提取工作线程,调用组件的 public service(ServletRequest, ServletResponse)。

  3. 该方法完成参数类型强转后,将控制权移交至 protected service(HttpServletRequest, HttpServletResponse)。

  4. protected service 读取 req.getMethod() 并判定动词分支。

  5. 流程跳转至子类重写的 doGet() 或 doPost() 方法执行实际业务。

  6. 开发者将响应数据写入 HttpServletResponse,工作线程将其重构为 HTTP 报文回传给客户端。

开发实践 ​

继承 HttpServlet 只需要按需覆写对应的处理逻辑。以下代码演示了 GET 查询与 POST 提交的分流处理:

java
package com.example;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/user")
public class UserServlet extends HttpServlet {
  // 覆写 doGet 方法处理客户端的查询读取请求
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setContentType("text/plain;charset=UTF-8");
    resp.getWriter().write("User details");
  }

  // 覆写 doPost 方法处理客户端的表单提交与数据持久化
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String username = req.getParameter("username");
    resp.getWriter().write("User created: " + username);
  }
}

API: HttpServlet ​

构造方法 ​

  • HttpServlet HttpServlet():(),默认无参构造方法。初始化空实例。通常由 Servlet 容器通过反射机制隐式调用。

    java
    package com.example.servlet.http.api;
    
    import javax.servlet.http.HttpServlet;
    
    public class ConstructorApiDemo extends HttpServlet {
      public ConstructorApiDemo() {
        // 调用父类无参构造方法
        super();
      }
    }

核心请求处理方法 ​

  • void doGet():(HttpServletRequest req, HttpServletResponse resp),处理 GET 请求。用于支持资源的幂等安全读取操作。

  • void doPost():(HttpServletRequest req, HttpServletResponse resp),处理 POST 请求。用于接收表单提交、新增数据以及包含请求体的非幂等操作。

  • void doPut():(HttpServletRequest req, HttpServletResponse resp),处理 PUT 请求。用于实现符合 RESTful 规范的全量替换或幂等更新操作。

  • void doDelete():(HttpServletRequest req, HttpServletResponse resp),处理 DELETE 请求。用于按 URI 移除指定资源的幂等操作。

  • void doHead():(HttpServletRequest req, HttpServletResponse resp),处理 HEAD 请求。仅获取与 GET 请求完全一致的响应头信息,用于检测资源存活或探测缓存时效。

  • void doOptions():(HttpServletRequest req, HttpServletResponse resp),处理 OPTIONS 请求。探测服务端针对目标资源所支持的所有 HTTP 动词,并在响应头写入 Allow 标头。

  • void doTrace():(HttpServletRequest req, HttpServletResponse resp),处理 TRACE 请求。将收到的请求报文回显给客户端,主要用于网络诊断(通常应禁用)。

注意事项:

  1. 严禁调用 super.doGet(req, resp) 等方法:在重写的 doGet()、doPost() 等业务代码中,如果保留了开发工具自动生成的 super.doGet(req, resp),由于父类内部的默认实现是输出 405,这会导致客户端直接收到 405 Method Not Allowed 错误。
  2. 编码设置时机限制:设置字符编码(如 req.setCharacterEncoding("UTF-8") 或 resp.setContentType("application/json;charset=UTF-8"))必须位于读取请求参数(如 req.getParameter(...))或获取输出流(如 resp.getWriter())之前执行,否则设置将被底层解析器忽略。
  3. 输出流互斥冲突:对于同一个响应对象,resp.getWriter() 和 resp.getOutputStream() 是严格互斥的。在一次请求生命周期中连续调用两者,将直接抛出 IllegalStateException: getOutputStream() has already been called for this response。
java
package com.example.servlet.http.api;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CoreMethodsApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    resp.setStatus(HttpServletResponse.SC_OK);
    resp.getWriter().write("READ");
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    resp.setStatus(HttpServletResponse.SC_CREATED);
    resp.getWriter().write("CREATE");
  }

  @Override
  protected void doPut(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
  }

  @Override
  protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
  }

  @Override
  protected void doHead(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    // 自定义 HEAD 响应,父类默认实现会代理至 doGet 并拦截响应体
    super.doHead(req, resp);
  }

  @Override
  protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    // 允许跨域预检标头或手动添加 Allow
    super.doOptions(req, resp);
    resp.setHeader("X-Custom-Capability", "Supported");
  }

  @Override
  protected void doTrace(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    // 显式拒绝 TRACE 动词以加固安全体系,阻止 XST 跨站追踪攻击
    resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "TRACE is disabled");
  }
}

流程调度与缓存辅助方法 ​

  • void service():(HttpServletRequest req, HttpServletResponse resp),保护分发入口。解析 HTTP 请求动词,决定将请求转发给哪一个具体 doXxx 模板方法。

  • long getLastModified():(HttpServletRequest req),获取最后修改时间。计算并返回当前请求目标的最后修改毫秒时间戳,用于配合客户端实现 HTTP 304 缓存协商。

注意事项:

  1. service 覆写的副作用:一旦重写了 protected void service(HttpServletRequest, HttpServletResponse) 且未显式调用 super.service(req, resp),所有的 doGet、doPost 等模板分发机制将彻底失效。

  2. getLastModified 时间粒度:HTTP 规范标准中对于时间的描述精度为秒(RFC 7231)。因此在重写 getLastModified 时,务必将毫秒截断或整除至秒级(例如 (time / 1000) * 1000),避免毫秒精度微小偏差造成条件判断失效。

  3. 不要混淆“时间单位(毫秒 vs 秒)”与“时间精度 / 粒度(精确到毫秒 vs 精确到整秒):

    staticAssetTimestamp 的单位依然是毫秒(13 位),并没有变成以秒为单位的数值(10 位)。

    这里的运算过程:

    java
    // 假设当前时间戳是:1710000000567 毫秒(末尾 567 为毫秒零头)
    long time = System.currentTimeMillis();             // 1710000000567
    long seconds = time / 1000;                        // 1710000000(整数除法,截断丢弃毫秒)
    long alignedMillis = seconds * 1000;               // 1710000000000(单位仍是毫秒,但末尾抹零)

    (System.currentTimeMillis() / 1000) * 1000 的实际效果是:将毫秒部分抹平归零,进行“整秒对齐”。

java
package com.example.servlet.http.api;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DispatchSupportApiDemo extends HttpServlet {

  private final long staticAssetTimestamp = (System.currentTimeMillis() / 1000) * 1000;

  @Override
  protected long getLastModified(HttpServletRequest req) {
    // 动态返回资源的修改时间戳,协助容器校验 If-Modified-Since
    return staticAssetTimestamp;
  }

  @Override
  protected void service(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    // 在调度至具体动词之前执行全局切面追踪
    long start = System.currentTimeMillis();
    try {
      super.service(req, resp);
    } finally {
      long duration = System.currentTimeMillis() - start;
      System.out.println("HTTP 请求调度耗时: " + duration + "ms");
    }
  }
}

Servlet 注解 ​

概述 ​

Servlet 3.0 规范引入了基于元数据的注解体系,彻底改变了以往完全依赖 web.xml 集中式配置的开发模式,实现了组件的就地声明与“零配置”(Zero-XML)开发。

演进背景:在 Servlet 2.5 及更早版本中,注册任何 Servlet、Filter 或 Listener 都必须在 WEB-INF/web.xml 中编写大量冗长的 XML 标签:

  • 传统 XML 的痛点:

    • 随着项目规模扩张,web.xml 迅速膨胀至数千行,团队多人协作时极易产生合并冲突;
    • 组件与配置分离,维护代码时需要频繁在 Java 源文件与 XML 间来回切换。
  • 注解驱动的优势:

    • 配置与类定义就近绑定,代码内聚性大幅提升;
    • 结合自动扫描机制,模块化打包(Web Fragment)时能够实现即插即用。

@WebServlet 核心路由 ​

@WebServlet 用于声明一个 Servlet 组件并将其挂载到特定的 URL 路径上,主要属性如下:

属性名称类型说明与默认值
urlPatterns / valueString[]映射的请求 URL 规则数组。两者互为别名,不能同时赋值
nameString组件注册名称,未指定时默认取类的全限定名
loadOnStartupint启动加载优先级。默认 -1(延迟到首次访问时实例化);≥0\ge 0 时容器启动即加载
asyncSupportedboolean是否支持异步请求处理模式,默认为 false
initParamsWebInitParam[]绑定到该 Servlet 实例私有的初始化参数数组
java
package com.example.servlet;

import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

// 声明 Servlet 路由映射并注入私有初始化配置参数
@WebServlet(
  name = "UserServlet",
  urlPatterns = {"/user", "/user/detail"},
  initParams = {
    @WebInitParam(name = "module", value = "order")
  }
)
public class UserServlet extends HttpServlet {
  // 读取注解绑定的私有初始化参数值
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    String module = getInitParameter("module");
    resp.getWriter().write("Module: " + module);
  }
}

@WebInitParam 参数注入 ​

@WebInitParam 必须配合 @WebServlet 或 @WebFilter 嵌套使用,无法独立标注在类上:

  • 作用域限定:通过该注解配置的键值对仅归属于当前被修饰的单个组件,各组件之间相互隔离,不影响全局上下文。
  • 核心属性:包含 name(参数名,必填)、value(参数值,必填)以及 description(描述信息,可选)。
  • 读取时机:由容器在调用 init(ServletConfig) 时解析装配,业务代码直接通过当前实例的 getInitParameter(name) 即可提取。

@WebFilter 链路过滤@ ​

@WebFilter 用于将类声明为过滤器组件,拦截匹配路径下的进出流量:

  • 拦截路径:通过 urlPatterns 匹配 URL 模式,或者通过 servletNames 绑定特定的 Servlet 组件名称。

  • 调度类型:通过 dispatcherTypes 属性可精确控制触发时机,涵盖 DispatcherType.REQUEST(常规请求)、FORWARD(服务端转发)、INCLUDE(包含页面)、ERROR(异常重定向)以及 ASYNC(异步调度)。

  • 排序注意点:@WebFilter 不支持直接在注解中指定过滤器的执行先后顺序。如果依赖特定的拦截顺序,通常依靠类名的字母自然排序,或者回退到 web.xml 中显式按顺序声明。

    java
    package com.example.filter;
    
    import jakarta.servlet.Filter;
    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletRequest;
    import jakarta.servlet.ServletResponse;
    import jakarta.servlet.annotation.WebFilter;
    import java.io.IOException;
    
    // 注册过滤器并配置统一拦截的路径规则
    @WebFilter(filterName = "TraceFilter", urlPatterns = "/*")
    public class TraceFilter implements Filter {
      @Override
      // 拦截进入容器的请求并传递给后续组件链
      public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException {
        chain.doFilter(req, res);
      }
    }

@WebListener 事件感知@ ​

@WebListener 用于将类注册为全局事件监听器:

  • 零属性配置:该注解不需要指定任何参数,直接标注在监听类上方即可。
  • 接口自动绑定:Tomcat 在类扫描时会自动检测该类实现了哪些监听接口(如 ServletContextListener、HttpSessionListener、ServletRequestListener),并自动注册到对应的事件触发链中。

@MultipartConfig 文件上传 ​

处理多部件文件上传(multipart/form-data)时,类上方必须显式标注 @MultipartConfig,容器才会启用内建的流解析机制:

  • location:指定临时缓存文件的物理磁盘路径。

  • maxFileSize:限制单次请求中单个文件的最大字节大小(如 1024 * 1024 * 10 表示 10MB)。

  • maxRequestSize:限制包含所有文件及普通表单项的整批请求的最大字节大小。

  • fileSizeThreshold:内存缓冲阈值。文件未超过该阈值时保存在内存中,超出部分将自动溢出暂存到临时磁盘。

    java
    package com.example.servlet;
    
    import jakarta.servlet.annotation.MultipartConfig;
    import jakarta.servlet.annotation.WebServlet;
    import jakarta.servlet.http.HttpServlet;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import jakarta.servlet.http.Part;
    import java.io.IOException;
    
    // 配置文件上传阈值与单次请求容量限制
    @MultipartConfig(
      maxFileSize = 1024 * 1024 * 10,
      maxRequestSize = 1024 * 1024 * 50
    )
    @WebServlet("/upload")
    public class UploadServlet extends HttpServlet {
      // 提取并处理客户端提交的二进制文件段
      @Override
      protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        Part filePart = req.getPart("file");
      }
    }

请求路径匹配模式 ​

在 Servlet 规范中,urlPatterns(或 web.xml 中的 <url-pattern>)用于定义客户端请求的 URI 映射规则。当 Web 容器(如 Tomcat)接收到 HTTP 请求时,会按照规范定义的标准化模式与严格的优先级来寻找目标 Servlet。

匹配机制:Web 容器接收到请求后,会解析请求路径(去除 Context Path 与 Query String),并在已注册的 Servlet 映射表中进行检索。

Servlet 规范共定义了四种匹配模式:精确匹配、路径匹配、扩展名匹配和缺省匹配。容器依据固定的优先级顺序进行单向匹配,一旦在某个阶段匹配成功,就会立即将请求分派给对应的 Servlet,不再继续向下检索。

模式1:精确匹配 ​

精确匹配要求客户端请求的路径与配置规则完全一致:

  • 语法规范:必须以正斜杠 / 开头,且字符串内部及结尾不能包含通配符 *。

  • 匹配示例:配置为 /order/detail 时,只有请求路径严格等于 /order/detail 才会命中;请求 /order/detail/ 或 /order/details 均无法匹配。

    java
    package com.example.servlet;
    
    import jakarta.servlet.annotation.WebServlet;
    import jakarta.servlet.http.HttpServlet;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    // 精确匹配:必须以斜杠开头且不能包含通配符
    @WebServlet(urlPatterns = "/order/detail")
    public class ExactServlet extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.getWriter().write("Exact Match");
      }
    }

模式2:路径匹配 ​

路径匹配用于匹配某一目录层级及其子路径下的所有请求:

  • 语法规范:必须以 / 开头,且必须以 /* 结尾。

  • 匹配示例:配置为 /user/* 时,请求 /user/list、/user/add、/user/role/info 均会命中。

  • 最长路径优先原则:若容器中同时存在多个路径规则均能匹配当前请求(例如 /user/* 与 /user/order/*),容器会选择字符匹配最长、目录层级最深的规则对应的 Servlet。

    java
    package com.example.servlet;
    
    import jakarta.servlet.annotation.WebServlet;
    import jakarta.servlet.http.HttpServlet;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    // 路径匹配:以斜杠开头并以星号结尾,支持最长路径优先原则
    @WebServlet(urlPatterns = "/user/*")
    public class PathServlet extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.getWriter().write("Path Match");
      }
    }

模式3:扩展名匹配 ​

扩展名匹配根据请求路径的后缀名进行过滤与转发:

  • 语法规范:必须以 *. 开头,后接具体的后缀名称,且绝对不能以 / 开头。

  • 匹配示例:配置为 *.do 时,请求 /user.do、/order/pay.do、/system/api/query.do 都会被截获处理。

  • 典型场景:早期 MVC 框架(如 Struts 1/2)常使用 *.action 或 *.do 作为前端控制器的拦截标识。

    java
    package com.example.servlet;
    
    import jakarta.servlet.annotation.WebServlet;
    import jakarta.servlet.http.HttpServlet;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    // 扩展名匹配:以星号点开头且绝对不能以斜杠开头
    @WebServlet(urlPatterns = "*.do")
    public class ExtensionServlet extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.getWriter().write("Extension Match");
      }
    }

模式4:缺省匹配 ​

缺省匹配(Default Match) 作为请求分发的保底机制存在:

  • 语法规范:配置路径仅为一个正斜杠 /。

  • 运作机制:当客户端请求在经历精确、路径和扩展名匹配后均未找到任何匹配项时,容器会将该请求交给缺省 Servlet 处理。

  • 核心价值:Tomcat 内部自带一个名为 default 的 DefaultServlet,配置即为 /。它的核心工作是在容器中查找 HTML、CSS、JS、图片等静态资源并直接输出。当在Web项目中配置了 urlPatterns = "/",会覆盖 Tomcat 默认的 DefaultServlet,导致静态资源无法访问,所有日常要避免使用它。

    java
    package com.example.servlet;
    
    import jakarta.servlet.annotation.WebServlet;
    import jakarta.servlet.http.HttpServlet;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    // 缺省匹配:在所有其他匹配模式未命中时作为兜底处理
    @WebServlet(urlPatterns = "/")
    public class DefaultServletDemo extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.getWriter().write("Default Fallback");
      }
    }

匹配顺序 ​

当一个请求到达容器时,Tomcat 严格按照以下由高到低的优先级顺序进行匹配:

  1. 精确匹配:首先检索是否有完全一致的精确路径。

  2. 路径匹配:未命中精确匹配时,依次检索前缀规则,多条规则重合时应用最长路径原则。

  3. 扩展名匹配:若路径前缀仍未命中,检查 URI 结尾是否存在匹配的扩展名规则。

  4. 缺省匹配:前三者均未命中时,交由配置为 / 的缺省 Servlet 处理。

  5. 404 响应:若缺省 Servlet 无法在磁盘中检索到对应的物理静态文件,容器返回 HTTP 404 状态码。

配置误区 ​

在编写 URL 匹配规则时,常见以下两类典型错误:

  • 混用路径与扩展名:配置形如 /*.do 或 /api/*.json 属于非法配置。Servlet 规范严格禁止在同一个模式中同时出现 / 与 *.,Tomcat 启动解析时会直接抛出 IllegalArgumentException 并导致应用部署失败。
  • 混淆 /* 与 /:
    • /* 属于路径匹配,其优先级高于扩展名匹配与缺省匹配。若将自定义 Servlet 映射为 /*,它将强行拦截所有请求(包括各类静态资源请求及 *.jsp),导致静态资源完全无法加载。
    • / 属于缺省匹配,优先级最低,仅在没有其他 Servlet 认领时生效,通常用于自定义静态资源处理器或由 Spring MVC 的 DispatcherServlet 接管未映射的普通请求。

权限安全 ​

Servlet 规范提供了声明式权限控制注解,直接与容器的安全域(Security Realm)联动:

  • @ServletSecurity:顶级安全注解,控制对被修饰 Servlet 的访问授权。
  • @HttpConstraint:定义默认的安全约束规则,通过 rolesAllowed = {"ADMIN"} 规定允许访问的角色集合,通过 transportGuarantee = TransportGuarantee.CONFIDENTIAL 强制当前端点必须走 HTTPS 加密通道。
  • @HttpMethodConstraint:针对特定 HTTP 动词(如仅限制 POST 需管理员角色,而开放 GET 查询)进行细粒度差异化权限约束。

注解扫描开关 ​

注解配置生效依赖 Web 容器在启动部署阶段的反射扫描机制:

  1. 应用部署启动时,Tomcat 会解析应用的 WEB-INF/web.xml 文件。

  2. 容器检查 <web-app> 根标签中的 metadata-complete 属性值。

  3. 若 metadata-complete="true",容器认定所有配置以 XML 为准,将彻底关闭注解扫描以节省启动耗时。

  4. 若 metadata-complete="false"(或缺省),容器会扫描 /WEB-INF/classes 目录下的所有类字节码,检索带有 @WebServlet、@WebFilter 等注解的类。

  5. 覆盖冲突法则:如果同一个组件或相同 URL 既在注解中声明又在 web.xml 中配置,容器以 web.xml 的声明为主,覆盖注解中的对应属性。

自定义 MiniTomcatContext ​

在 Tomcat 底层,Web 应用上下文(Context)负责管理所有组件。Tomcat 在部署应用时,通过字节码扫描或反射机制检索带有 @WebServlet 的类,完成实例化并通过内部映射表(如 Mapper 与 Wrapper 容器)建立访问路径与实例的对应关系。

核心原理:简化版容器模型主要由以下三个逻辑部分协同运作:

  • 元数据定义:提供 @WebServlet 注解,声明 URL 映射规则。
  • 解析与容器存储:读取类上的注解,反射创建单例对象并存入 Map<String, Servlet>。
  • 分发调用:接收请求路径,通过哈希表直接 O(1) 定位目标 Servlet 并执行业务逻辑。

MiniTomcatContext 业务流程时序图:

image-20260905155511347

步骤1:注解定义 ​

定义一个轻量级的运行时注解,用于挂载路由地址:

java
package com.example.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

// 运行时保留以支持容器通过反射读取元数据
@Retention(RetentionPolicy.RUNTIME)
public @interface WebServlet {
  // 映射客户端访问的 URL 路径规则
  String[] urlPatterns() default {};
  String[] value() default {};
}

步骤2:容器解析 ​

模拟 Tomcat 核心上下文的加载逻辑,通过反射完成实例化并注入全局哈希表:

java
package com.example.container;

import com.example.annotation.WebServlet;
import jakarta.servlet.Servlet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MiniTomcatContext {
  private final Map<String, Servlet> servletMapping = new HashMap<>();

  // 扫描指定的类集合并解析注解元数据
  public void loadServlets(List<Class<?>> classes) throws Exception {
    for (Class<?> clazz : classes) {
      if (clazz.isAnnotationPresent(WebServlet.class)) {
        WebServlet anno = clazz.getAnnotation(WebServlet.class);
        // 反射调用无参构造函数创建 Servlet 实例
        Servlet servletInstance = (Servlet) clazz.getDeclaredConstructor().newInstance();
        String[] urls = anno.urlPatterns().length > 0 ? anno.urlPatterns() : anno.value();
        for (String url : urls) {
          // 将访问路径与创建的 Servlet 实例存入全局映射表
          servletMapping.put(url, servletInstance);
        }
      }
    }
  }

  // 根据请求路径从映射表中获取目标 Servlet 实例
  public Servlet getServlet(String path) {
    return servletMapping.get(path);
  }
}

步骤3:路由分发 ​

当 HTTP 请求到达时,容器直接根据请求的 URI 从 HashMap 中提取对应的单例对象执行 service 方法:

java
package com.example.container;

import jakarta.servlet.Servlet;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

public class RequestDispatcher {
  public void dispatch(MiniTomcatContext context, String uri, ServletRequest req, ServletResponse res) throws Exception {
    // 从容器上下文中根据路径查找目标处理组件
    Servlet targetServlet = context.getServlet(uri);
    if (targetServlet != null) {
      // 调用对应 Servlet 的统一入口方法响应请求
      targetServlet.service(req, res);
    }
  }
}

自定义 @WebServlet@ ​

实现自定义 @WebServlet 的核心在于注解元数据载入与 Servlet 容器的动态注册机制。原生 @WebServlet 之所以能生效,是因为 Tomcat 容器在启动时扫描了类字节码,并通过内部 API 将其注册到请求分发树中。

借助 Servlet 3.0 提供的 ServletContext.addServlet() 动态注册能力与反射机制,可以轻量化复刻该流程。

核心原理:自定义注解与加载引擎的运转分为三个核心步骤:

  • 元数据定义:通过 Java 元注解(@Target、@Retention)定义一个能够在运行时读取的自定义注解。
  • 组件扫描与反射:在容器启动阶段定位带有该注解的业务类,通过反射提取路由路径与组件名。
  • 动态装配挂载:调用 ServletContext 提供的编程式注册方法 addServlet() 与 addMapping(),将组件注入容器并生效。

自定义注解从声明到生效的底层流转流程时序图:

image-20260905160108646

步骤1:注解定义 ​

定义 @MyWebServlet 注解,声明其修饰目标为类(ElementType.TYPE),并设置生命周期保留至运行时(RetentionPolicy.RUNTIME):

java
package com.example.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 声明注解作用于类型且保留至运行时阶段
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyWebServlet {
  // 映射客户端访问路径规则
  String[] urlPatterns() default {};
  String[] value() default {};
  String name() default "";
}

步骤2:业务编写 ​

编写普通的业务 Servlet 并继承 HttpServlet,在类上方标注刚刚声明的 @MyWebServlet:

java
package com.example.servlet;

import com.example.annotation.MyWebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

// 使用自定义注解绑定路由路径并指定组件名称
@MyWebServlet(urlPatterns = {"/custom-hello"}, name = "CustomHelloServlet")
public class CustomHelloServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // 业务逻辑处理并直接输出响应文本
    resp.setContentType("text/plain;charset=UTF-8");
    resp.getWriter().write("Hello from custom @MyWebServlet!");
  }
}

步骤3:动态注册 ​

在应用初始化阶段,通过 ServletContextListener 拦截 Web 应用启动事件,扫描目标类并执行动态注册:

java
package com.example.listener;

import com.example.annotation.MyWebServlet;
import com.example.servlet.CustomHelloServlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.ServletRegistration;
import jakarta.servlet.annotation.WebListener;
import java.util.List;

@WebListener
public class CustomWebAnnotationListener implements ServletContextListener {
  // 监听 Web 容器启动并在初始化阶段执行扫描与注册
  @Override
  public void contextInitialized(ServletContextEvent sce) {
    ServletContext context = sce.getServletContext();
    List<Class<?>> targetClasses = List.of(CustomHelloServlet.class);

    for (Class<?> clazz : targetClasses) {
      // 判断当前类是否标注了自定义注解并提取元数据
      if (clazz.isAnnotationPresent(MyWebServlet.class)) {
        MyWebServlet anno = clazz.getAnnotation(MyWebServlet.class);
        String servletName = anno.name().isEmpty() ? clazz.getSimpleName() : anno.name();
        String[] urls = anno.urlPatterns().length > 0 ? anno.urlPatterns() : anno.value();
        // 动态向上下文注册组件实例并完成 URL 映射挂载
        @SuppressWarnings("unchecked")
        ServletRegistration.Dynamic reg =
            context.addServlet(servletName, (Class<? extends jakarta.servlet.Servlet>) clazz);
        reg.addMapping(urls);
      }
    }
  }
}