Skip to content

Servlet 与 Java Web 请求全过程

Servlet 是 Java Web 容器与业务代码之间的标准契约。Tomcat 负责网络监听、HTTP 解析、线程调度和对象创建,Servlet 负责在 service(request,response) 中处理一次应用层请求。Spring MVC 的 DispatcherServlet、Jersey 的入口以及很多老系统接口都建立在这套规范上。

学习目标

  • 从 TCP 连接进入 Tomcat 开始,讲清请求如何找到 Servlet;
  • 理解 Servlet 实例、容器工作线程、Request、Response 的生命周期;
  • 掌握 URL 映射优先级、上下文路径、Servlet Path 和 Path Info;
  • 解释 init/service/destroy 的调用条件、并发边界和失败后果;
  • 正确读取参数、请求体、请求头,正确设置状态码、编码和响应体;
  • 区分 forward、include、redirect,理解“响应已提交”为什么不能回头;
  • 理解 Servlet 3 异步处理能解决什么、不能解决什么;
  • 能排查 404、405、400、乱码、请求体为空、线程串数据和容器线程耗尽。

一、Servlet、容器和 HTTP 各自负责什么

mermaid
flowchart TD
    A["客户端建立TCP连接"] --> B["Tomcat Connector接收字节"]
    B --> C["解析HTTP请求行请求头和请求体"]
    C --> D["创建容器内部Request与Response"]
    D --> E["Adapter进入Servlet容器管线"]
    E --> F["按Context和URL映射Servlet"]
    F --> G["Filter链"]
    G --> H["Servlet.service"]
    H --> I["业务写入Response"]
    I --> J["容器生成HTTP响应字节"]
    J --> K["通过连接返回客户端"]
角色主要职责
HTTP定义请求方法、路径、头、状态码和消息体语义
Servlet 规范定义生命周期、Request/Response、Filter、Session、异步等 Java API 契约
Tomcat/Jetty/Undertow实现规范,并提供网络、线程池、类加载、部署和连接管理
应用 Servlet根据请求执行应用逻辑并构造响应

Servlet 不负责直接监听 8080 端口,也不亲自解析原始 TCP 字节。它接收到的是容器已经包装好的 HttpServletRequestHttpServletResponse

二、规范版本与包名边界

Servlet 版本和 JDK 版本不是同一维度:

规范代表能力常见时代
Servlet 2.5主要依赖 web.xml 配置老 Java EE 项目
Servlet 3.0注解、异步、文件上传、容器初始化扩展常见 JDK 7/8 项目
Servlet 3.1非阻塞 I/O APIJava EE 7
Servlet 4.0HTTP/2 相关能力Java EE 8
Jakarta Servlet 5+包名从 javax.servlet 改为 jakarta.servletJakarta EE 9+、Tomcat 10+

老项目常见:

java
import javax.servlet.http.HttpServlet;

Tomcat 10+ 常见:

java
import jakarta.servlet.http.HttpServlet;

这不是只改一个 import 就总能升级成功:第三方 Filter、Listener、JSP 标签库和框架也必须使用同一包体系。Tomcat 9 支持 javax 时代规范,Tomcat 10 转入 jakarta;部署不匹配时可能出现类找不到、接口类型不兼容或应用启动失败。

三、Web 应用和上下文路径

假设浏览器访问:

text
http://example.com:8080/order-app/api/orders/100?detail=true

可拆为:

部分示例含义
schemehttp应用层协议
hostexample.com主机名
port8080服务端口
context path/order-appWeb 应用在容器中的部署根路径
application path/api/orders/100在应用内部用于映射的路径
query stringdetail=true查询字符串

常见 API:

java
request.getRequestURI();  // /order-app/api/orders/100
request.getContextPath(); // /order-app
request.getServletPath(); // 取决于映射方式
request.getPathInfo();    // 取决于映射方式
request.getQueryString(); // detail=true

不要把 getRequestURLgetRequestURIgetServletPath 混用。反向代理后,外部 scheme/host 还可能与容器看到的内部地址不同,需要正确处理 Forwarded/X-Forwarded-*,并只信任受控代理写入的头。

四、URL 映射是怎样选择的

Servlet 可通过 web.xml 或注解注册。

4.1 web.xml 方式

xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="3.1">
    <servlet>
        <servlet-name>orderServlet</servlet-name>
        <servlet-class>com.example.OrderServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>orderServlet</servlet-name>
        <url-pattern>/api/orders/*</url-pattern>
    </servlet-mapping>
</web-app>

4.2 Servlet 3 注解方式

java
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;

@WebServlet(
        name = "orderServlet",
        urlPatterns = "/api/orders/*",
        loadOnStartup = 1,
        asyncSupported = true
)
public class OrderServlet extends HttpServlet {
}

同一个 Servlet 名称、URL 映射发生冲突时,应用可能部署失败或按规范配置规则处理,不能假定“后定义覆盖前定义”。大型项目应统一注册方式,避免扫描注解和 XML 分散维护。

4.3 四类映射与优先级

text
精确映射       /api/orders/100
路径映射       /api/orders/*
扩展名映射     *.do
默认映射       /

选择顺序通常是:

mermaid
flowchart TD
    A["取得应用内请求路径"] --> B{"存在精确匹配吗"}
    B -- "是" --> C["选择精确映射"]
    B -- "否" --> D{"存在最长路径匹配吗"}
    D -- "是" --> E["选择最长的前缀路径映射"]
    D -- "否" --> F{"存在扩展名匹配吗"}
    F -- "是" --> G["选择扩展名映射"]
    F -- "否" --> H["使用默认Servlet映射"]

路径映射会选择最长匹配,例如 /api/*/api/orders/* 同时存在时,后者处理 /api/orders/100

4.4 servletPath 与 pathInfo

若映射是 /api/orders/*,请求 /api/orders/100 通常得到:

text
servletPath = /api/orders
pathInfo    = /100

若是精确映射 /api/orders/100servletPath 通常是完整映射路径,pathInfo 为 null。路由代码若依赖 pathInfo,必须根据实际映射方式测试,不能用字符串硬切整个 URI。

五、Servlet 生命周期全过程

mermaid
flowchart TD
    A["应用部署"] --> B{"loadOnStartup大于等于0吗"}
    B -- "是" --> C["启动时实例化"]
    B -- "否" --> D["首次匹配请求时实例化"]
    C --> E["构造Servlet对象"]
    D --> E
    E --> F["注入ServletConfig"]
    F --> G["调用init一次"]
    G --> H["并发调用service多次"]
    H --> I["应用停止重载或卸载"]
    I --> J["停止接收并等待在途请求"]
    J --> K["调用destroy一次"]
    K --> L["实例等待回收"]

5.1 实例何时创建

  • 配置 load-on-startup 非负值时,容器通常部署阶段创建并初始化,数值用于相对启动顺序;
  • 未配置或为负值时,常见行为是首次请求时懒加载;
  • 同一个 Servlet 定义通常对应一个实例,但规范和容器部署模型决定细节,不能依赖构造器做外部注册;
  • 应用重新部署会使用新类加载器和新实例。

5.2 init 做什么

init(ServletConfig) 只调用一次,适合验证本 Servlet 的只读配置、构建线程安全且可关闭的资源。初始化失败会导致 Servlet 不可用,启动加载场景下可使应用部署失败。

java
@Override
public void init() throws javax.servlet.ServletException {
    String endpoint = getServletConfig().getInitParameter("endpoint");
    if (endpoint == null) {
        throw new javax.servlet.ServletException("endpoint is required");
    }
}

不建议在 init 中执行无限等待的远程调用。配置中心或数据库故障时,应用会卡在启动阶段,发布平台只看到实例不健康。应设置严格超时、给出明确异常并根据依赖重要性决定失败启动还是降级。

5.3 service 如何分派 HTTP 方法

容器调用 HttpServlet.service,它根据 HTTP 方法分派到 doGet/doPost/doPut/doDelete/doHead/doOptions/doTrace。没有实现相应方法时通常返回 405,而不是自动改用 doPost。

不要直接重写 service 后忘记调用父类,否则可能绕过标准方法分派、条件请求和 HEAD 行为。只有确实需要统一控制整个分派过程时才重写。

5.4 destroy 的真实边界

destroy 用于停止本 Servlet 创建的后台任务、关闭资源和注销注册。它不是绝对保证:进程被 kill -9、机器断电或 JVM 崩溃时不会执行,因此关键数据不能只依赖 destroy 刷盘。

资源最好由应用级 Listener、依赖注入容器或连接池统一管理,避免每个 Servlet 重复创建数据库连接池或线程池。

六、单实例、多线程与线程安全

Servlet 初始化完成后,容器工作线程可并发调用同一个实例的 service

mermaid
flowchart TD
    A["同一个OrderServlet实例"] --> B["http线程1处理订单A"]
    A --> C["http线程2处理订单B"]
    A --> D["http线程3处理订单C"]
    B --> E["共享Servlet成员字段"]
    C --> E
    D --> E

错误代码:

java
public class UnsafeServlet extends javax.servlet.http.HttpServlet {
    private String currentUser;

    protected void doGet(javax.servlet.http.HttpServletRequest request,
                         javax.servlet.http.HttpServletResponse response) {
        currentUser = request.getParameter("user");
        // 另一个请求可能已覆盖 currentUser
    }
}

安全边界:

  • 请求数据放方法局部变量;
  • Request/Response 只在当前请求链内使用;
  • 成员字段只保存初始化后不变的不可变配置或线程安全组件;
  • 不把 StringBuilder、普通 HashMap、当前用户、当前订单等可变请求状态放成员字段;
  • ServletContext 和 Session 中的对象同样可能被多个线程访问,容器只管理属性容器,不自动让 value 内部线程安全。

给整个 servicesynchronized 虽可能避免字段竞态,却会把所有请求串行化,吞吐和延迟恶化;正确做法是消除共享可变请求状态。

七、HttpServletRequest 全过程

Request 是容器对当前请求的包装,生命周期通常到请求完成;异步场景会延长。不要保存到静态字段、异步任务或缓存中长期使用。

7.1 参数来自哪里

getParameter 可能组合查询字符串和 application/x-www-form-urlencoded 表单字段。多个同名参数使用 getParameterValues。JSON 请求体不会自动变成 parameter,需要用输入流读取并由 JSON 库反序列化。

java
String id = request.getParameter("id");
String[] tags = request.getParameterValues("tag");
String contentType = request.getContentType();
String method = request.getMethod();

客户端传入的参数、头、Cookie 都不可信,必须做长度、格式、范围、权限和业务校验。不能把 X-User-Id 之类普通请求头直接当已认证身份。

7.2 编码设置必须在读取参数之前

java
request.setCharacterEncoding("UTF-8");
String name = request.getParameter("name");

对表单 body,容器第一次解析参数时会按当时编码解码;先读取参数再设置编码通常已经晚了。GET 查询串编码还受容器 Connector 配置和 Servlet 规范版本影响,不能只靠 setCharacterEncoding 解决所有历史乱码。

7.3 请求体只能消费一次

java
javax.servlet.ServletInputStream input = request.getInputStream();

原始 body 是流。读取后游标向前,后续 Controller/Servlet 再读可能为空。getInputStream()getReader() 不能在同一请求上混用,通常会抛 IllegalStateException

Filter 若要记录 body 并让下游继续读取,必须实现有大小上限的缓存包装器;不能直接读完,也不能无上限缓存上传文件,否则会破坏业务或造成 OOM。敏感字段、密码、token 和医疗数据不能原样写日志。

7.4 属性不是参数

java
request.setAttribute("order", orderDto);
Object value = request.getAttribute("order");
  • parameter 来自客户端请求数据,通常是字符串;
  • attribute 由服务端代码在当前请求中设置,可保存对象;
  • forward 时仍使用同一 Request,因此 attribute 可共享;
  • redirect 是新请求,原 Request attribute 不存在。

7.5 客户端 IP 不能盲信头

getRemoteAddr() 通常得到与容器直接连接的对端,经过网关时可能是代理 IP。X-Forwarded-For 可被客户端伪造,只有网络拓扑保证请求必须经过受控代理,且应用按可信代理链规则解析时才能使用。

八、HttpServletResponse 全过程

Response 在容器内有缓冲区。状态码、响应头和 body 在提交前可修改;发生以下情况之一可能提交:

  • 缓冲区写满并 flush;
  • 应用显式 flushBuffer()
  • Writer/OutputStream 被 flush/close;
  • forward/请求结束时容器提交;
  • 某些 sendError/sendRedirect 操作触发提交语义。
mermaid
flowchart TD
    A["创建未提交Response"] --> B["设置状态码响应头和编码"]
    B --> C["向响应缓冲写body"]
    C --> D{"缓冲满flush或请求结束吗"}
    D -- "否" --> C
    D -- "是" --> E["提交状态行和响应头"]
    E --> F["继续发送响应体"]
    F --> G["提交后不能可靠修改状态和头"]

8.1 编码顺序

java
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
java.io.PrintWriter writer = response.getWriter();
writer.write("{\"message\":\"成功\"}");

应在 getWriter() 之前设置编码和 Content-Type。Writer 创建后字符到字节的编码器通常已经确定,之后修改可能无效。

8.2 Writer 与 OutputStream 互斥

文本使用 getWriter(),二进制下载使用 getOutputStream()。同一响应同时调用二者通常抛 IllegalStateException。下载还应设置正确 Content-Type、Content-Disposition,并防止文件名响应头注入。

8.3 状态码不是总返回 200

场景常见状态码
查询成功200
创建成功201
无响应体成功204
参数格式错误400
未认证401
已认证但无权限403
资源不存在404
方法不支持405
冲突或版本冲突409
服务端未知错误500
服务暂不可用503

业务错误全部返回 HTTP 200 会让网关、监控和客户端重试无法正确判断;但状态码与业务错误码应共同设计,不能只靠状态码表达全部业务细节。

8.4 sendError 和异常页

sendError(status,message) 通知容器按错误处理机制生成响应,可能清空缓冲并触发 error-page。若响应已提交,容器无法重写已经发送的状态和头,可能只中断连接或记录异常。

九、forward、include 与 redirect

维度forwardincluderedirect
谁执行下一步服务端容器服务端容器浏览器/客户端
HTTP 请求次数通常 1 次通常 1 次至少新增 1 次
地址栏不变不变改变
Request 对象同一个同一个新请求
request attribute可共享可共享不共享
可跨站否,通常当前上下文资源可以,但要防开放重定向

9.1 forward

java
request.setAttribute("order", order);
request.getRequestDispatcher("/WEB-INF/views/order.jsp")
       .forward(request, response);
return;

forward 后应立即 return,避免继续写响应。若此前已 flush 导致 response committed,会抛 IllegalStateException。容器在 forward 前通常清理未提交响应缓冲,但不会撤销已经执行的数据库操作。

9.2 include

include 把另一个资源的输出包含进当前响应,被包含资源不能随意改变最终状态码和关键响应头。常用于传统 JSP 片段,现代 JSON API 较少直接使用。

9.3 redirect

java
String location = response.encodeRedirectURL(
        request.getContextPath() + "/orders/100");
response.sendRedirect(location);

redirect 返回 3xx 和 Location,客户端重新发请求。不能直接把用户输入拼进 Location,否则会形成开放重定向钓鱼漏洞。POST 后重定向可使用 PRG 模式避免浏览器刷新重复提交,但最终仍需服务端幂等。

十、完整 Servlet 3 Demo

下面用内存 Map 模拟订单查询,演示映射、路径解析、校验、编码、状态码和线程安全边界。真实项目把业务放 Service,Servlet/Controller 只处理协议转换。

java
package com.example.web;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(name = "orderServlet", urlPatterns = "/api/orders/*", loadOnStartup = 1)
public class OrderServlet extends HttpServlet {
    private Map<String, String> orders;

    @Override
    public void init() throws ServletException {
        Map<String, String> data = new HashMap<String, String>();
        data.put("100", "PAID");
        data.put("101", "CREATED");
        orders = Collections.unmodifiableMap(data); // 初始化后安全发布只读对象
    }

    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");

        String pathInfo = request.getPathInfo();
        if (pathInfo == null || !pathInfo.matches("/[0-9]{1,20}")) {
            writeError(response, HttpServletResponse.SC_BAD_REQUEST,
                    "invalid order id");
            return;
        }

        String orderId = pathInfo.substring(1);
        String status = orders.get(orderId);
        if (status == null) {
            writeError(response, HttpServletResponse.SC_NOT_FOUND,
                    "order not found");
            return;
        }

        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().write(
                "{\"id\":\"" + orderId + "\",\"status\":\"" + status + "\"}");
    }

    private void writeError(HttpServletResponse response,
                            int status,
                            String message) throws IOException {
        response.setStatus(status);
        response.getWriter().write("{\"error\":\"" + message + "\"}");
    }
}

教学代码中的 message 是固定字符串。真实系统拼接用户内容时必须使用 JSON 库转义,避免生成非法 JSON 或注入;订单数据应由 Service/DAO 提供,不应在 Servlet 中手写数据库连接。

十一、Servlet 3 异步处理

同步 Servlet 中,容器工作线程从进入 service 到响应结束一直被占用。异步 Servlet 可调用 startAsync() 暂时退出原容器线程,待异步任务完成后调用 complete() 或 dispatch。

mermaid
flowchart TD
    A["容器线程进入Servlet"] --> B["调用request.startAsync"]
    B --> C["原容器线程返回线程池"]
    C --> D["异步任务等待或执行"]
    D --> E["异步线程写Response或dispatch"]
    E --> F["调用AsyncContext.complete"]
    F --> G["容器提交响应并回收请求"]
java
@WebServlet(urlPatterns = "/async-report", asyncSupported = true)
public class AsyncReportServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws IOException {
        final javax.servlet.AsyncContext async = request.startAsync();
        async.setTimeout(3000L);
        async.start(new Runnable() {
            public void run() {
                try {
                    async.getResponse().setContentType("text/plain;charset=UTF-8");
                    async.getResponse().getWriter().write("report ready");
                } catch (Exception e) {
                    // 记录任务ID和异常,按统一错误策略处理
                } finally {
                    async.complete();
                }
            }
        });
    }
}

关键边界:

  • 调用链中的 Servlet 和 Filter 都要支持 async;
  • 必须设置超时并注册 AsyncListener 处理完成、超时和错误;
  • 每条路径只正确 complete 一次;
  • 异步不等于无线程,AsyncContext.start 仍使用容器管理的线程;
  • 不要把慢数据库查询变成无限异步任务,数据库连接容量没有增加;
  • 原请求线程的 ThreadLocal、MDC、SecurityContext 不会自动安全传播到另一线程;
  • 异步开始后 Request/Response 生命周期延长,但 complete 后绝不能继续使用;
  • 客户端断开不保证已经提交的业务自动取消。

Servlet 3.1 的非阻塞 ReadListener/WriteListener 又是另一层 API,用于容器通知可读可写;不能把 Servlet 3.0 异步简单等同于 NIO 非阻塞 I/O。

十二、ServletContext 与 ServletConfig

对象范围主要用途
ServletConfig一个 Servlet 定义Servlet 名称、init-param、ServletContext
ServletContext一个 Web 应用应用参数、资源、属性、日志、动态注册入口

ServletContext attribute 在整个应用共享,可能被多个请求线程访问。放入不可变配置或线程安全服务可以,不能放一个普通 HashMap 后让所有请求无同步修改。

应用级资源的初始化与销毁更适合 ServletContextListener,下一章会讲 Listener 的事件顺序、并发边界和类加载泄漏。

十三、Spring MVC 与 Servlet 的关系

mermaid
flowchart TD
    A["Tomcat解析HTTP"] --> B["Filter链"]
    B --> C["DispatcherServlet.service"]
    C --> D["HandlerMapping查找Controller"]
    D --> E["HandlerAdapter调用方法"]
    E --> F["参数解析与校验"]
    F --> G["Controller调用Service"]
    G --> H["返回值处理"]
    H --> I["HttpMessageConverter序列化"]
    I --> J["写入HttpServletResponse"]

Spring MVC 没有绕过 Servlet。DispatcherServlet 是前端控制器 Servlet,把所有请求集中后完成映射、参数绑定、异常处理、视图解析和消息转换。Spring Boot 内嵌 Tomcat 只是把容器作为依赖启动,Servlet 生命周期和线程安全原则仍成立。

Spring Bean 默认也常是单例,因此 Controller/Service 同样不应保存当前用户等请求可变状态。Spring RequestContext、事务和安全上下文大量依赖 ThreadLocal,手动异步时尤其要关注上下文传播与清理。

十四、扩展点与启动发现

扩展点作用
@WebServlet / web.xml声明 Servlet 和映射
ServletContainerInitializer容器启动时发现框架并执行初始化
WebApplicationInitializerSpring 对 Servlet 3 初始化机制的封装接口
ServletContext.addServlet启动阶段动态注册 Servlet
Filter请求进入 Servlet 前后拦截
Listener监听应用、Session、Request 生命周期事件
AsyncListener监听异步完成、超时、错误、重新开始

ServletContainerInitializer 通常通过 JAR 中 META-INF/services/javax.servlet.ServletContainerInitializer 被容器发现,并可配合 @HandlesTypes 扫描关注类型。扫描范围和类路径很大时会增加启动耗时,框架要谨慎定义发现范围。

十五、生产故障排查 Runbook

15.1 404:到底哪一层没找到

mermaid
flowchart TD
    A["请求返回404"] --> B{"域名端口和Context正确吗"}
    B -- "否" --> C["修正入口或部署路径"]
    B -- "是" --> D{"应用部署成功吗"}
    D -- "否" --> E["查启动日志和类加载异常"]
    D -- "是" --> F{"Servlet映射命中吗"}
    F -- "否" --> G["查URL pattern和代理重写"]
    F -- "是" --> H["查业务是否主动返回404"]

记录完整外部 URL、代理转发后的内部 path、context path、应用部署日志和访问日志。不能看到 404 就只检查 Controller 注解。

15.2 405 Method Not Allowed

确认客户端实际方法,是否只实现了 doGet 却发送 POST,OPTIONS 预检是否被正确处理,代理是否改写方法。不要为了消除 405 把所有方法都转给一个处理逻辑,否则可能破坏幂等和权限语义。

15.3 参数为空或请求体为空

检查 Content-Type 与客户端编码;JSON 不能用 getParameter 获取;是否有 Filter 提前读取 body;是否混用 Reader/InputStream;是否 multipart;Content-Length/Transfer-Encoding 和代理限制是否异常。

15.4 中文乱码

分别确定乱码发生在请求查询串、表单 body、JSON body、响应 Writer、数据库还是日志。检查设置编码的时间点,抓取原始字节和 Content-Type;不要在已经错误解码成 String 后反复 getBytes 猜编码。

15.5 Cannot forward after response has been committed

检查是否在 forward 前写入大量响应、flush、关闭 Writer,或异常处理器试图在响应提交后再次重写。记录 response.isCommitted() 和首次写出位置,重构为“先决定结果,再统一写响应”。

15.6 用户数据串号

审查 Servlet/Controller 单例成员字段、静态变量、共享 DTO、ThreadLocal 清理。连续并发压测复现,日志使用请求 ID 但不能输出敏感用户信息。成员字段改局部变量;ThreadLocal 必须 finally remove。

15.7 请求大量卡住

  1. 看容器当前线程、最大线程、accept/backlog 和连接数;
  2. 连续取得 jstack,按 http-nio-* 等线程分组;
  3. 栈停在 socketRead 查下游超时,停在数据库连接池查连接等待,BLOCKED 查锁,CPU 高查业务循环/序列化;
  4. 对比请求进入 TPS、完成 TPS、P95/P99、下游延迟和拒绝数;
  5. 不要直接提高 Tomcat maxThreads,把数据库或下游打得更慢。

15.8 应用重部署后内存不释放

检查 Servlet/Listener 创建的线程未停、ThreadLocal、JDBC Driver、定时器、静态缓存和第三方库是否由公共类加载器持有 WebAppClassLoader。destroy 里关闭资源,优先使用容器管理生命周期,并通过堆转储查看到旧 WebAppClassLoader 的引用链。

十六、常见错误与后果

错误后果正确做法
Servlet 成员字段保存当前用户并发串数据方法局部变量
先 getParameter 再设置编码表单已错误解码解析前统一设置
Filter 读完 body 不包装Servlet 再读为空有上限缓存包装
同时 getWriter 和 getOutputStreamIllegalStateException按文本/二进制二选一
flush 后再 forward/sendError响应已提交先决策后统一输出
redirect 拼接任意用户 URL开放重定向白名单和站内相对地址
init 中无限调远程服务应用启动卡死超时、失败策略和健康检查
async 不 completeRequest/Response 和资源泄漏超时监听与 finally 完成
每个 Servlet 创建线程池资源失控、停机泄漏应用统一管理

十七、面试标准回答

Servlet 生命周期是什么

容器按启动加载或首次请求实例化 Servlet,注入 ServletConfig 后调用一次 init;初始化成功后多个容器线程可并发调用同一实例的 service,HttpServlet 再按方法分派到 doGet/doPost;应用卸载或停止时容器等待在途请求并调用一次 destroy,但进程崩溃时不保证执行。

Servlet 为什么不是线程安全的

通常同一实例被多个请求线程并发调用。方法局部变量属于各自调用,但成员字段由所有请求共享;把当前用户、订单或可变缓冲放成员字段会产生覆盖和串数据。Servlet 类不是天然不安全,关键是不能保存未同步的请求级可变状态。

forward 和 redirect 区别

forward 是容器内部使用同一个 Request/Response 转交,浏览器通常只有一次请求、地址栏不变、attribute 可共享;redirect 返回 3xx 和 Location,让客户端重新请求,地址改变、原 Request 数据不保留,还要防开放重定向。

Response committed 是什么意思

表示状态行和响应头已开始发送给客户端,之后无法可靠修改状态、编码和头,也不能再 forward 或用统一错误页重写。缓冲写满、flush、close、sendRedirect 或请求结束都可能触发提交。

Servlet 异步是否等于非阻塞

不等于。Servlet 3.0 AsyncContext 允许释放原请求线程并在其他线程完成响应,但异步任务仍需要线程,慢 SQL 仍占数据库连接。Servlet 3.1 ReadListener/WriteListener 才提供面向流的非阻塞通知 API。

十八、关联知识与验收

完成以下任务才算掌握:

  • 能从 Connector 收到 HTTP 字节画到 Servlet.service 和响应提交;
  • 能根据四类映射算出命中的 Servlet、servletPath 和 pathInfo;
  • 能解释 load-on-startup、懒加载、init 失败和 destroy 不保证执行;
  • 能复现成员字段串数据并改成无共享状态设计;
  • 能说明 body 为什么只能读一次、Response 何时 committed;
  • 能写 JDK 7/8 兼容的 Servlet 3 Demo,正确返回 400、404、200;
  • 面对 404、405、乱码、body 为空和线程耗尽,能说出证据链和第一步检查,而不是只会重启 Tomcat。