理解传统 Java Web,真正关键的不是记多少 Servlet API,而是搞清楚:
这也是后面理解 Spring MVC、过滤器链和嵌入式 Tomcat 的基础。
Servlet 容器本质上是 Java Web 应用和 HTTP 世界之间的桥梁。它主要负责:
Filter、Servlet、Listener常见 Servlet 容器包括:
| 容器 | 特点 |
|---|---|
| Tomcat | 最主流,Spring Boot 默认嵌入式容器,社区活跃 |
| Jetty | 轻量、可嵌入性好,适合长连接/WebSocket 场景 |
| Undertow | 高性能、非阻塞,Spring Boot 可选替换容器 |
在现代 Spring Boot 项目中,虽然很多开发者不再直接写 Servlet,但底层依然离不开这套容器模型。
可以先把一次请求粗略理解为下面几个阶段:
浏览器 → DNS → TCP连接 → HTTP请求 → 容器接收 → 线程分配 → Filter链 → Servlet → 业务逻辑 → 写响应 → 返回浏览器详细阶段:
HttpServletRequest 和 HttpServletResponseweb.xml 或注解配置的顺序)如果中间发生了:
那么实际路径还会发生变化。
客户端 Servlet容器 Servlet Filter
│ │ │ │
│── HTTP Request ────────>│ │ │
│ │── 创建 Request/Response ──>│ │
│ │── 从线程池分配线程 ────────>│ │
│ │ │ │
│ │── Filter1.doFilter() ─────────────────────────────>│
│ │ │ ┌── chain.doFilter() ──┐
│ │ │ │── Filter2.doFilter() │
│ │ │ │── chain.doFilter() ──┤
│ │ │<────────────────┘ │
│ │── Servlet.service() ─────>│ │
│ │<── 响应 ──────────────────│<───────────────────────────────────────│
│<── HTTP Response ──────│ │ │
│ │── 线程回收 ───────────────>│ │ServletContextServletContext 可以理解为整个 Web 应用级别的上下文。它通常在应用启动时创建,在应用关闭时销毁。
它适合保存:
// 在 Servlet 中获取 ServletContext
ServletContext context = getServletContext();
// 获取初始化参数
String dbUrl = context.getInitParameter("dbUrl");
// 设置全局属性
context.setAttribute("onlineCount", 100);生命周期:
contextInitialized())contextDestroyed())常见用途:
| 用途 | 说明 |
|---|---|
| 全局配置 | 存放数据库连接、系统参数等 |
| 共享数据 | 多个 Servlet 之间共享数据 |
| 资源读取 | 读取 web.xml 中的配置 |
| 请求转发 | getRequestDispatcher() 实现转发 |
ServletServlet 是处理请求的核心组件。容器负责创建 Servlet 实例,并在收到匹配请求时调用其 service() 方法。
Servlet 生命周期:
加载 → 实例化 → 初始化(init) → 请求处理(service) → 销毁(destroy)@WebServlet("/demo")
public class DemoServlet extends HttpServlet {
// 1. 初始化(只调用一次)
@Override
public void init() throws ServletException {
System.out.println("Servlet 初始化");
}
// 2. 请求处理(每次请求都调用)
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 根据 HTTP 方法分发到 doGet / doPost 等
super.service(req, resp);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().write("Hello Servlet");
}
// 3. 销毁(只调用一次)
@Override
public void destroy() {
System.out.println("Servlet 销毁");
}
}需要注意:
init() 在第一次请求或启动时调用(取决于 loadOnStartup 配置)destroy() 在容器关闭时调用,用于释放资源FilterFilter 位于业务 Servlet 之前或之后,适合处理横切逻辑,例如:
Filter 执行顺序:
请求: Filter1 → Filter2 → Filter3 → Servlet
响应: Servlet → Filter3 → Filter2 → Filter1@WebFilter(urlPatterns = "/*")
public class AuthFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter 初始化
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
// 前置处理:编码设置
req.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
// 前置处理:登录校验
String uri = req.getRequestURI();
HttpSession session = req.getSession(false);
if (!uri.contains("/login") && session == null) {
response.getWriter().write("未登录");
return; // 不放行
}
// 放行:继续执行后续 Filter 或 Servlet
long start = System.currentTimeMillis();
chain.doFilter(request, response);
long cost = System.currentTimeMillis() - start;
// 后置处理:记录耗时
System.out.println(req.getRequestURI() + " 耗时: " + cost + "ms");
}
@Override
public void destroy() {
// Filter 销毁
}
}ListenerListener 用于监听应用、会话或请求级事件,例如:
// 监听 Session 创建和销毁
@WebListener
public class SessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println("Session 创建: " + se.getSession().getId());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
System.out.println("Session 销毁: " + se.getSession().getId());
}
}常用 Listener 类型:
| Listener | 监听事件 | 用途 |
|---|---|---|
ServletContextListener | 应用启动/销毁 | 初始化数据库连接池、加载缓存 |
HttpSessionListener | Session 创建/销毁 | 在线用户统计 |
ServletRequestListener | 请求进入/结束 | 请求日志 |
HttpSessionAttributeListener | Session 属性变化 | 监听登录状态变化 |
Servlet 容器通常不会为每个请求临时新建线程,而是通过线程池复用工作线程。因此:
这也是为什么 Java Web 和并发问题天然相关。
如果请求里存在这些操作:
都可能把容器线程池拖满,最终表现为:
┌─────────────────────────────────┐
│ Tomcat 线程池 │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │Thread1│ │Thread2│ │Thread3│ │ ← 默认最大200个线程
│ └───────┘ └───────┘ └───────┘ │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │Thread4│ │Thread5│ │ ... │ │
│ └───────┘ └───────┘ └───────┘ │
├─────────────────────────────────┤
│ 连接器(Acceptor) → Endpoint │ ← NIO 模型接收连接
│ Protocol Handler → Coyote │ ← HTTP 协议解析
│ Container → Engine │ ← Servlet 容器
└─────────────────────────────────┘关键配置参数:
<!-- server.xml
maxThreads: 最大线程数
minSpareThreads: 最小空闲线程
acceptCount: 等待队列长度 -->
<Connector port="8080"
protocol="HTTP/1.1"
maxThreads="200"
minSpareThreads="10"
acceptCount="100"
connectionTimeout="20000" />当所有线程都在忙,新请求会进入等待队列(acceptCount),队列满后请求会被拒绝。
请求转发发生在服务端内部,浏览器通常只发起一次请求。请求对象会继续沿用当前链路。
// 请求转发
RequestDispatcher dispatcher = request.getRequestDispatcher("/user/detail");
dispatcher.forward(request, response);特点:
Request 对象forward 前不能提交响应重定向是服务端告诉浏览器"你再发一个新的请求到另一个地址"。因此浏览器会再次发起请求,地址栏也会变化。
// 重定向
response.sendRedirect("/login.jsp");特点:
Request 对象| 维度 | 转发 (Forward) | 重定向 (Redirect) |
|---|---|---|
| 请求次数 | 1次 | 2次 |
| 地址栏 | 不变 | 变化 |
| Request 共享 | √ 共享 | × 不共享 |
| 跳转范围 | 当前应用内 | 任意URL |
| WEB-INF | √ 可以访问 | × 不可以访问 |
| 效率 | 高 | 较低 |
它们最大的区别可以简单记为:
如果 Servlet 容器线程被慢请求占满,即使业务代码本身没挂,整体吞吐也会明显下降。
排查步骤:
1. 检查线程池状态
- jstack 分析线程状态
- 查看线程是否都在 WAITING / BLOCKED
2. 定位慢请求
- 访问日志找高耗时接口
- Filter 打印每个请求耗时
3. 分析根因
- 慢 SQL?
- 外部调用超时?
- 阻塞 IO?
- 数据库连接池耗尽?这时排查不能只看 Controller 或 Servlet 逻辑,还要看:
很多鉴权、日志、编码、跨域处理都发生在业务 Servlet 之前。不理解请求生命周期,很容易排障跑偏,以为"请求没进 Controller 就说明服务没收到请求"。
排查思路:
请求 → DNS → LB → Nginx → Tomcat → Filter1 → Filter2 → Servlet
排查时逐层检查:
1. 网络层:DNS是否正常?LB是否健康?
2. 容器层:Tomcat是否收到请求?access log?
3. 过滤器层:哪个Filter拦截了?打印日志确认
4. Servlet层:路由是否匹配?参数是否正确?看似都是"跳页面",实际一个发生在服务端内部链路,一个依赖浏览器再发一次请求。
典型错误:
// 错误:forward 之后又尝试写响应
request.getRequestDispatcher("/success.jsp").forward(request, response);
response.getWriter().write("hello"); // × IllegalStateException
// 正确:forward 之后直接 return
request.getRequestDispatcher("/success.jsp").forward(request, response);
return; // √遇到请求链路问题时,优先看:
@WebServlet 或 web.xml)jstack 线程 dump)maxThreads、acceptCount)@WebFilter 无法控制顺序,需要 web.xml)Servlet 容器主要负责什么?
一个 HTTP 请求在 Java Web 里大致经历哪些阶段?
为什么 Servlet 模型和线程安全问题天然相关?
过滤器为什么通常发生在业务逻辑之前?
Servlet 的生命周期是怎样的?
init() → service() / doGet() / doPost() → destroy()转发和重定向的区别?
如何解决 Servlet 线程安全问题?
ThreadLocal线上接口 RT 突然升高,如何排查?
jstack 分析线程状态Tomcat 线程池满了会怎样?
acceptCount)Connection Refused 或 503ServletContext 和 Session 的区别?
ServletContext:应用级别,所有用户共享Session:会话级别,单个用户独享| 特性 | 旧版(本文编写时) | 当前(Servlet 6.0,Tomcat 10.1+) |
|---|---|---|
| 容器演进 | Tomcat 8/9 | Tomcat 10.1/11(jakarta 命名空间) |
| 请求线程模型 | 阻塞式线程池 | 可启用虚拟线程(Boot 3.2+/Tomcat 10.1+) |
| HTTP/2 | Servlet 4.0 支持 | Servlet 6.0 持续支持 HTTP/2 |
| 生命周期 | init/service/destroy | 机制不变 |
| 部署 | 外部容器 | Spring Boot 内嵌容器为主流 |
Servlet 容器请求生命周期(解析→Filter 链→Servlet 实例化→service)在 Servlet 6.0 中保持不变,本文对生命周期各阶段的理解依然适用。