这是一份非常详细、实用、通俗易懂、权威且全面的 Java Servlet 指南,涵盖了其方方面面,包括在 Spring Boot 中的应用,并提供了可直接在 IDE 中运行的最佳实践代码和完整案例。
目录
- Servlet 概述
- 1.1 什么是 Servlet?
- 1.2 为什么需要 Servlet?
- 1.3 Servlet 与 CGI 的比较
- 1.4 Servlet 在 Web 应用中的位置
- Servlet API 核心
- 2.1
javax.servlet包 (jakarta.servlet) - 2.2
javax.servlet.http包 (jakarta.servlet.http)
- 2.1
- Servlet 生命周期
- 3.1
init(ServletConfig config) - 3.2
service(ServletRequest req, ServletResponse res) - 3.3
destroy() - 3.4 生命周期图示与线程模型
- 3.1
- HttpServlet 详解
- 4.1 继承结构
- 4.2 核心方法:
doGet,doPost,doPut,doDelete,doHead,service - 4.3 处理 HTTP 请求参数
- HttpServletRequest
- 5.1 获取请求信息 (方法、URI、协议、头信息)
- 5.2 获取请求参数 (
getParameter,getParameterValues,getParameterMap) - 5.3 获取请求体 (InputStream/Reader)
- 5.4 请求属性 (
setAttribute,getAttribute) - 5.5 请求分发 (
RequestDispatcher)
- HttpServletResponse
- 6.1 设置状态码 (
setStatus,sendError) - 6.2 设置响应头 (
setHeader,addHeader,setContentType,setCharacterEncoding) - 6.3 获取输出流 (
getOutputStream,getWriter) - 6.4 重定向 (
sendRedirect) - 6.5 响应内容类型与编码
- 6.1 设置状态码 (
- 会话管理 (HttpSession)
- 7.1 会话的概念 (Cookie vs URL Rewriting vs Session)
- 7.2 获取或创建 Session (
request.getSession()) - 7.3 在 Session 中存储/获取/移除数据 (
setAttribute,getAttribute,removeAttribute) - 7.4 会话超时配置 (
session-timeout) - 7.5 使 Session 失效 (
invalidate())
- Servlet 配置
- 8.1
ServletConfig - 8.2
web.xml部署描述符配置- 声明与映射 Servlet (
<servlet>,<servlet-mapping>) - 初始化参数 (
<init-param>) - 加载顺序 (
<load-on-startup>)
- 声明与映射 Servlet (
- 8.3 基于注解的配置 (
@WebServlet)
- 8.1
- Servlet 上下文 (ServletContext)
- 9.1 应用范围共享数据 (
setAttribute,getAttribute) - 9.2 获取应用初始化参数 (
getInitParameter) - 9.3 获取资源路径 (
getRealPath) - 9.4 日志记录 (
log)
- 9.1 应用范围共享数据 (
- Servlet 过滤器 (Filter)
- 10.1 过滤器的概念与作用 (日志、安全、编码转换、压缩等)
- 10.2 过滤器生命周期 (
init,doFilter,destroy) - 10.3 配置过滤器 (
web.xml或@WebFilter) - 10.4 过滤器链 (
FilterChain)
- Servlet 监听器 (Listener)
- 11.1 监听器的概念与作用
- 11.2 主要监听器接口 (
ServletContextListener,HttpSessionListener,ServletRequestListener等) - 11.3 配置监听器 (
web.xml或@WebListener)
- 异步 Servlet
- 12.1 传统 Servlet 的阻塞模型限制
- 12.2 异步处理支持 (
@WebServlet(asyncSupported = true)) - 12.3
AsyncContext接口 (startAsync,complete,dispatch) - 12.4 使用场景 (长轮询、服务器推送、长时间计算)
- Servlet 与 Spring Boot
- 13.1 Spring Boot 对 Servlet 的支持 (嵌入式容器:Tomcat, Jetty, Undertow)
- 13.2 自动配置 Servlet 组件 (
ServletRegistrationBean,FilterRegistrationBean,ServletListenerRegistrationBean) - 13.3 在 Spring Boot 中定义 Servlet/Filter/Listener
- 使用
@ServletComponentScan+@WebServlet/@WebFilter/@WebListener - 使用
@Bean+ServletRegistrationBean/FilterRegistrationBean/ServletListenerRegistrationBean
- 使用
- 13.4 Spring MVC DispatcherServlet 与 Servlet 的关系
- 13.5 在 Spring Boot 中访问 Servlet API (
HttpServletRequest,HttpServletResponse,HttpSession)
- Servlet 最佳实践
- 14.1 线程安全注意事项
- 14.2 资源管理与关闭流
- 14.3 异常处理
- 14.4 性能考虑
- 14.5 安全考虑 (XSS, CSRF, SQL 注入)
- 实战案例
- 案例一:基础用户登录与会话管理
- 案例二:文件上传 Servlet
- 案例三:异步 Servlet 实现简单消息推送
- 案例四:Spring Boot 中自定义 Filter 实现请求日志与耗时统计
1. Servlet 概述
1.1 什么是 Servlet?
Servlet 是运行在 Web 服务器或应用服务器上的 Java 程序 。它充当了客户端(通常是 Web 浏览器)请求 和服务器上应用程序(数据库、Java 类等) 之间的中间层 。简单来说,Servlet 是用来扩展服务器功能 、处理 HTTP 请求 并生成 HTTP 响应的 Java 类。
1.2 为什么需要 Servlet?
- 动态内容生成: 相比静态 HTML,Servlet 可以根据请求参数、数据库查询结果等动态生成 HTML、JSON、XML 等内容。
- 高效: Servlet 在服务器端加载后驻留内存,处理后续请求速度快(相比传统的 CGI 为每个请求创建新进程)。
- 平台独立: Java 的 "Write Once, Run Anywhere" 特性使得 Servlet 可以在任何支持 Java 的服务器上运行。
- 功能强大: 提供丰富的 API 处理请求、响应、会话、上下文、过滤器等。
- 可扩展性: 是构建 Java Web 应用(如 JSP、JSF、Spring MVC)的基础。
1.3 Servlet 与 CGI 的比较
| 特性 | CGI | Servlet |
|---|---|---|
| 进程模型 | 每个请求创建一个新进程 | 多线程处理请求(轻量级) |
| 性能 | 启动慢,资源消耗大 | 启动快,资源消耗小 |
| 平台 | 依赖语言和平台 (Perl, C, Python) | 基于 Java,平台独立 |
| 持久性 | 无 | 驻留内存,可保持状态 |
| 安全性 | 较低 | 较高(Java 安全机制) |
1.4 Servlet 在 Web 应用中的位置
浏览器 (HTTP Request) -> Web Server (Tomcat, Jetty) -> Servlet Container -> Servlet
|
Servlet (HTTP Response) <- Servlet Container <- Web Server <- 浏览器 (HTTP Response)
- Web 服务器: 处理网络连接(TCP/IP, HTTP)。
- Servlet 容器 (Servlet Engine): 管理 Servlet 生命周期,处理请求/响应。常见容器:Tomcat, Jetty, Undertow。
- Servlet: 业务逻辑处理单元。
2. Servlet API 核心
Servlet API 主要由两个包组成:
2.1 javax.servlet 包 (jakarta.servlet)
- 包含与协议无关的核心接口和类。
- 核心接口:
Servlet: 定义 Servlet 生命周期方法 (init,service,destroy)。ServletConfig: 提供 Servlet 初始化参数和ServletContext。ServletContext: 代表整个 Web 应用,用于应用级别的共享和资源访问。ServletRequest,ServletResponse: 表示请求和响应的通用接口。RequestDispatcher: 用于请求转发或包含。Filter: 定义过滤器接口。FilterChain: 表示过滤器链。
- 注意: Jakarta EE 9 及以后,包名从
javax.servlet迁移到jakarta.servlet。使用 Tomcat 10+ 或 Jakarta EE 9+ 项目时需注意。
2.2 javax.servlet.http 包 (jakarta.servlet.http)
- 提供特定于 HTTP 协议的接口和类。
- 核心接口和类:
HttpServlet: 继承自GenericServlet(实现了Servlet),提供处理 HTTP 方法 (doGet,doPost等) 的骨架。HttpServletRequest: 扩展ServletRequest,提供 HTTP 特有的请求信息(方法、URI、头、Cookie、Session)。HttpServletResponse: 扩展ServletResponse,提供 HTTP 特有的响应功能(状态码、重定向、头、Cookie)。HttpSession: 用于维护用户会话状态。Cookie: 表示 HTTP Cookie。
3. Servlet 生命周期
Servlet 的生命周期由 Servlet 容器管理,包含三个关键方法:
3.1 init(ServletConfig config)
- 调用时机: Servlet 实例被创建后,在第一次处理请求之前 ,由容器调用一次。
- 目的: 执行一次性初始化 工作。
- 读取配置参数 (
ServletConfig.getInitParameter()) - 建立数据库连接池(通常不推荐在此处直接创建,建议使用连接池管理)
- 加载资源
- 读取配置参数 (
- 参数:
ServletConfig对象,包含 Servlet 的配置信息(如初始化参数)和ServletContext引用。 - 注意: 可以覆盖无参数的
init()方法,但通常使用带ServletConfig参数的版本。
3.2 service(ServletRequest req, ServletResponse res)
- 调用时机: 每次客户端请求到达时,由容器调用。
- 目的: 处理客户端的请求并生成响应。
- 参数:
ServletRequest和ServletResponse对象,分别封装了请求和响应信息。在HttpServlet中,通常由更具体的doGet、doPost等方法处理。 - 线程模型: 容器通常会为每个请求创建一个新线程 来调用
service方法。因此,Servlet 必须是线程安全的!避免使用实例变量存储请求相关的状态。
3.3 destroy()
- 调用时机: Servlet 实例即将被销毁时(例如应用卸载或服务器关闭),由容器调用一次。
- 目的: 执行清理 工作。
- 关闭数据库连接(如果在此处打开了)
- 释放其他长期持有的资源
- 保存状态到持久存储(如果需要)
3.4 生命周期图示与线程模型
+-------------------+ +-------------------+
| Servlet Container | | Threads |
+-------------------+ +-------------------+
| |
| (First Request) |
v |
+---------------------+ |
| Instantiate Servlet | |
+---------------------+ |
| |
| init(config) |
v |
+---------------------+ |
| Servlet Ready | |
+---------------------+ |
| |
/---------- | ----------\ |
/ | \ |
+-------v----+ +-----v------+ +---v-------+ |
| Request 1 | | Request 2 | | Request n | ... |
+------------+ +------------+ +-----------+ |
| | | |
| service(req, res) | | (Each in own thread)
v v v |
+-------------------------------+ |
| Process Request & Generate Response | |
+-------------------------------+ |
| | | |
v v v |
+------------+ +------------+ +-----------+ |
| Response 1 | | Response 2 | | Response n| ... |
+------------+ +------------+ +-----------+ |
| |
| (App Unload/Server Stop) |
v |
+---------------------+ |
| destroy() | |
+---------------------+ |
| |
v |
+---------------------+ |
| Servlet Destroyed | |
+---------------------+ |
- 关键点:
init和destroy各调用一次,service为每个请求调用一次。多个请求的service方法可能并发执行。
4. HttpServlet 详解
HttpServlet 是开发 HTTP Servlet 最常用的基类。它继承自 GenericServlet。
4.1 继承结构
java.lang.Object
|
+-- GenericServlet (implements Servlet, ServletConfig, Serializable)
|
+-- HttpServlet
4.2 核心方法
service(HttpServletRequest req, HttpServletResponse resp): 覆盖了GenericServlet.service(ServletRequest, ServletResponse)。它根据 HTTP 请求方法 (GET,POST,PUT,DELETE,HEAD等) 调用相应的doXxx方法。doGet(HttpServletRequest req, HttpServletResponse resp): 处理 HTTP GET 请求。通常用于安全、幂等的操作,如查询数据。默认实现返回 HTTP 405 (Method Not Allowed) 错误。doPost(HttpServletRequest req, HttpServletResponse resp): 处理 HTTP POST 请求。通常用于提交数据 或执行非幂等操作(如创建资源)。默认实现返回 HTTP 405 错误。doPut(HttpServletRequest req, HttpServletResponse resp): 处理 HTTP PUT 请求。通常用于更新整个资源。默认实现返回 HTTP 405 错误。doDelete(HttpServletRequest req, HttpServletResponse resp): 处理 HTTP DELETE 请求。通常用于删除资源。默认实现返回 HTTP 405 错误。doHead(HttpServletRequest req, HttpServletResponse resp): 处理 HTTP HEAD 请求。与 GET 类似,但只返回头信息,不返回响应体。默认实现调用doGet并忽略响应体输出。doOptions(HttpServletRequest req, HttpServletResponse resp): 处理 HTTP OPTIONS 请求,返回服务器支持的 HTTP 方法。默认实现返回允许的方法列表。doTrace(HttpServletRequest req, HttpServletResponse resp): 处理 HTTP TRACE 请求(用于诊断)。默认实现返回客户端发送的请求信息。通常禁用。
最佳实践: 创建自定义 Servlet 时,继承 HttpServlet 并覆盖需要处理的 HTTP 方法对应的 doXxx 方法(最常见的是 doGet 和 doPost)。
4.3 处理 HTTP 请求参数
请求参数可以通过 URL 查询字符串 (GET) 或请求体 (POST, PUT) 传递。在 HttpServletRequest 中统一处理:
String getParameter(String name): 获取单个参数值。如果参数不存在返回null。String[] getParameterValues(String name): 获取多值参数(如复选框)的所有值。如果参数不存在返回null。Map<String, String[]> getParameterMap(): 获取所有请求参数的 Map,键为参数名,值为字符串数组。Enumeration<String> getParameterNames(): 获取所有请求参数名的枚举。
注意: 处理参数时需注意字符编码,通常需要设置请求的编码(如 request.setCharacterEncoding("UTF-8"))以正确解析非 ASCII 字符。但 setCharacterEncoding 必须在 getParameter 之前调用才有效。
5. HttpServletRequest
HttpServletRequest 对象封装了客户端发送的所有 HTTP 请求信息。
5.1 获取请求信息
- HTTP 方法:
String getMethod() - 请求 URI:
String getRequestURI()(如/myapp/servlet/Hello) - 上下文路径 (Context Path):
String getContextPath()(如/myapp) - Servlet 路径 (Servlet Path):
String getServletPath()(如/servlet/Hello) - 路径信息 (Path Info):
String getPathInfo()(URI 中位于 Servlet 路径之后、查询字符串之前的部分,可为null) - 查询字符串 (Query String):
String getQueryString()(如name=John&age=30) - 协议:
String getProtocol() - 远程地址:
String getRemoteAddr(),String getRemoteHost() - 请求头:
String getHeader(String name),Enumeration<String> getHeaderNames(),int getIntHeader(String name),long getDateHeader(String name) - Cookie:
Cookie[] getCookies()
5.2 获取请求参数
如前所述 (getParameter, getParameterValues, getParameterMap)。
5.3 获取请求体
ServletInputStream getInputStream(): 获取二进制输入流(用于文件上传、非文本数据)。BufferedReader getReader(): 获取字符输入流(用于文本数据,如表单 POST、JSON)。注意:getInputStream和getReader不能同时调用。
5.4 请求属性 (setAttribute, getAttribute)
- 属性存储在
request对象中,仅在当前请求范围内有效(从请求开始到生成响应结束)。 - 常用于在 Servlet 之间或 Servlet 与 JSP 之间传递数据(通过
RequestDispatcher)。 void setAttribute(String name, Object object)Object getAttribute(String name)void removeAttribute(String name)Enumeration<String> getAttributeNames()
5.5 请求分发 (RequestDispatcher)
用于将请求转发 (forward) 或包含 (include) 到另一个 Web 组件(Servlet、JSP 或 HTML)。
-
获取 RequestDispatcher:
RequestDispatcher getRequestDispatcher(String path):path必须是相对路径(相对于当前上下文根),可以包含其他 Servlet 或 JSP。RequestDispatcher getNamedDispatcher(String name): 通过 Servlet 名称获取。
-
转发 (forward):
javaRequestDispatcher dispatcher = request.getRequestDispatcher("/path/to/next"); dispatcher.forward(request, response);- 将请求完全交给目标资源处理,当前 Servlet 不再生成响应。响应由目标资源生成。
- 浏览器地址栏 URL 不会改变。
- 只能转发到同一应用内的资源。
-
包含 (include):
javaRequestDispatcher dispatcher = request.getRequestDispatcher("/path/to/include"); dispatcher.include(request, response);- 目标资源的输出被包含在当前 Servlet 的响应中。当前 Servlet 可以继续生成响应内容。
- 常用于模板页面的公共部分(如页眉、页脚)。
6. HttpServletResponse
HttpServletResponse 对象用于生成发送回客户端的 HTTP 响应。
6.1 设置状态码
-
void setStatus(int sc): 设置响应状态码 (如SC_OK200,SC_NOT_FOUND404)。常量定义在HttpServletResponse接口中。 -
void sendError(int sc): 发送错误状态码,并可选择包含错误消息。javaresponse.sendError(HttpServletResponse.SC_NOT_FOUND, "Resource not found"); -
void sendError(int sc, String msg): 同上,并指定错误消息。
6.2 设置响应头
void setHeader(String name, String value): 设置一个响应头。如果该头已存在,则覆盖。void addHeader(String name, String value): 添加一个响应头。如果该头允许多个值,则追加。void setContentType(String type): 设置响应内容类型 (text/html,application/json等)。非常重要,它告诉浏览器如何解释响应体。void setCharacterEncoding(String charset): 设置响应体的字符编码 (如UTF-8)。通常结合setContentType使用:response.setContentType("text/html;charset=UTF-8");。void setDateHeader(String name, long date): 设置包含日期值的响应头。void setIntHeader(String name, int value): 设置包含整数值的响应头。- 常用头示例:
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");(禁用缓存)response.setHeader("Pragma", "no-cache");(兼容旧版 HTTP/1.0)response.setHeader("Expires", "0");response.setHeader("Location", "http://www.example.com/newpage");(用于重定向)response.addCookie(Cookie cookie)(设置 Cookie,内部处理 Set-Cookie 头)
6.3 获取输出流
ServletOutputStream getOutputStream(): 获取用于写入二进制数据(如图片、文件)的输出流。PrintWriter getWriter(): 获取用于写入文本数据(如 HTML, JSON)的字符输出流。- 注意:
getOutputStream和getWriter不能同时调用。选择哪个取决于要发送的内容类型。
6.4 重定向 (sendRedirect)
java
response.sendRedirect("http://www.example.com/newpage"); // 绝对URL
response.sendRedirect("/myapp/newpath"); // 相对于上下文根的URL
- 告诉浏览器去请求一个新的 URL。浏览器地址栏会改变。
- 相当于设置状态码为
302 Found(或SC_MOVED_TEMPORARILY) 和Location头。 - 与转发 (forward) 不同,重定向是客户端的二次请求。
6.5 响应内容类型与编码
- 务必在获取输出流 之前 设置内容类型 (
setContentType) 和字符编码 (setCharacterEncoding或包含在setContentType中)。 - 常见 MIME 类型:
text/html: HTML 文档text/plain: 纯文本application/json: JSON 数据application/xml: XML 数据image/jpeg,image/png,image/gif: 图片application/pdf: PDF 文档application/octet-stream: 二进制流 (文件下载)
7. 会话管理 (HttpSession)
HTTP 是无状态协议。会话管理用于在多个 HTTP 请求间跟踪用户状态。
7.1 会话的概念
- Session: 服务器端为每个用户创建的一个会话对象,用于存储用户相关的数据。
- Session ID: 唯一标识一个会话的字符串。通常通过 Cookie (
JSESSIONID) 或 URL 重写 在客户端和服务器间传递。- Cookie (默认方式): 容器自动创建
JSESSIONIDCookie。浏览器每次请求都会带上它。 - URL 重写: 在 URL 后附加
;jsessionid=...。用于浏览器禁用 Cookie 的情况。可通过response.encodeURL(String url)或response.encodeRedirectURL(String url)自动添加。
- Cookie (默认方式): 容器自动创建
7.2 获取或创建 Session
java
HttpSession session = request.getSession(); // 获取现有session,如果没有则创建一个新的。
HttpSession session = request.getSession(false); // 获取现有session,如果没有则返回null。
7.3 在 Session 中存储/获取/移除数据
- 存储数据:
void setAttribute(String name, Object value) - 获取数据:
Object getAttribute(String name)(不存在返回null) - 移除数据:
void removeAttribute(String name) - 获取所有属性名:
Enumeration<String> getAttributeNames()
7.4 会话超时配置
-
容器级别 (web.xml):
XML<session-config> <session-timeout>30</session-timeout> <!-- 超时时间(分钟),30分钟 --> </session-config> -
Session 级别 (编程):
javasession.setMaxInactiveInterval(60 * 30); // 设置超时时间(秒),30分钟 int interval = session.getMaxInactiveInterval(); // 获取当前超时时间(秒)
7.5 使 Session 失效 (invalidate())
- 当用户注销或会话需要结束时调用。
- 移除该 Session 对象及其所有属性。
java
session.invalidate();
8. Servlet 配置
Servlet 可以通过传统的 web.xml 文件或注解进行配置。
8.1 ServletConfig
- 每个 Servlet 实例都有自己唯一的
ServletConfig对象。 - 在
init(ServletConfig config)方法中由容器传入。 - 主要方法:
String getServletName(): 获取 Servlet 名称。ServletContext getServletContext(): 获取ServletContext。String getInitParameter(String name): 获取 Servlet 初始化参数。Enumeration<String> getInitParameterNames(): 获取所有初始化参数名。
8.2 web.xml 部署描述符配置
声明 Servlet (<servlet>):
XML
<servlet>
<servlet-name>HelloServlet</servlet-name> <!-- 唯一标识名 -->
<servlet-class>com.example.HelloServlet</servlet-class> <!-- 完整类名 -->
<init-param> <!-- 可选:初始化参数 -->
<param-name>message</param-name>
<param-value>Welcome!</param-value>
</init-param>
<load-on-startup>1</load-on-startup> <!-- 可选:启动时加载顺序 (数字越小优先级越高) -->
</servlet>
映射 Servlet (<servlet-mapping>):
XML
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name> <!-- 指向上面定义的servlet-name -->
<url-pattern>/hello</url-pattern> <!-- 匹配的URL模式,如 /hello, /greet/*, *.do -->
</servlet-mapping>
8.3 基于注解的配置 (@WebServlet)
- Servlet 3.0+ 引入了注解,简化配置。
- 在 Servlet 类上使用
@WebServlet注解:
java
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
@WebServlet(
name = "AnnotationServlet", // 可选
urlPatterns = {"/annotated", "/anno/*"}, // 必需:URL 模式数组
initParams = { // 可选:初始化参数
@WebInitParam(name = "param1", value = "value1"),
@WebInitParam(name = "param2", value = "value2")
},
loadOnStartup = 1 // 可选:加载顺序
)
public class AnnotationServlet extends HttpServlet {
// ... doGet, doPost 等方法
}
- 优点: 简洁,Servlet 定义和映射在一起。
- 注意: 容器需要支持 Servlet 3.0+。在
web.xml中设置<metadata-complete>true</metadata-complete>会禁用注解扫描。
9. Servlet 上下文 (ServletContext)
ServletContext 代表整个 Web 应用程序。每个 Web 应用只有一个 ServletContext 对象 。它在应用启动时创建,应用卸载时销毁。所有 Servlet、Filter、Listener 共享同一个 ServletContext。
9.1 应用范围共享数据
- 存储数据:
void setAttribute(String name, Object object) - 获取数据:
Object getAttribute(String name)(不存在返回null) - 移除数据:
void removeAttribute(String name) - 获取所有属性名:
Enumeration<String> getAttributeNames()
9.2 获取应用初始化参数
在 web.xml 的 <context-param> 中定义:
XML
<context-param>
<param-name>jdbcUrl</param-name>
<param-value>jdbc:mysql://localhost:3306/mydb</param-value>
</context-param>
在 Servlet 中获取:
java
String jdbcUrl = getServletContext().getInitParameter("jdbcUrl");
9.3 获取资源路径
-
String getRealPath(String path): 将相对于 Web 应用根目录的虚拟路径转换为文件系统的绝对路径。常用于访问WEB-INF目录下的文件。javaString configPath = getServletContext().getRealPath("/WEB-INF/config.properties"); -
URL getResource(String path): 获取资源的 URL。 -
InputStream getResourceAsStream(String path): 获取资源的输入流。常用于读取类路径或WEB-INF下的文件。javaInputStream is = getServletContext().getResourceAsStream("/WEB-INF/config.properties");
9.4 日志记录 (log)
void log(String msg): 记录一般信息。void log(String message, Throwable throwable): 记录错误信息和异常堆栈。- 日志输出位置由容器配置(通常是服务器的日志文件)。
10. Servlet 过滤器 (Filter)
过滤器在请求到达 Servlet (或其他资源) 之前,或响应发送给客户端之前,拦截请求和响应 。用于执行横切关注点 (Cross-cutting Concerns)。
10.1 过滤器的概念与作用
- 日志记录: 记录请求、响应信息,统计耗时。
- 安全: 身份验证、授权检查。
- 数据转换: 请求/响应字符编码设置、数据压缩。
- 审计: 记录用户操作。
- 调试: 调试信息注入。
10.2 过滤器生命周期
类似于 Servlet:
init(FilterConfig filterConfig): 初始化时调用一次。doFilter(ServletRequest request, ServletResponse response, FilterChain chain): 每次请求/响应时调用。必须 调用chain.doFilter(request, response)将请求传递给链中的下一个组件(可能是另一个过滤器或目标资源)。destroy(): 销毁时调用一次。
10.3 配置过滤器
web.xml 配置:
XML
<filter>
<filter-name>LoggingFilter</filter-name>
<filter-class>com.example.LoggingFilter</filter-class>
<init-param>
<param-name>logLevel</param-name>
<param-value>INFO</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>LoggingFilter</filter-name>
<url-pattern>/*</url-pattern> <!-- 过滤所有请求 -->
<!-- 也可用 <servlet-name> 指定特定 Servlet -->
</filter-mapping>
注解配置 (@WebFilter):
java
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.Filter;
@WebFilter(
filterName = "AnnotatedFilter",
urlPatterns = {"/*"}, // 过滤所有请求
initParams = {
@WebInitParam(name = "encoding", value = "UTF-8")
}
)
public class EncodingFilter implements Filter {
// ... init, doFilter, destroy 方法
}
10.4 过滤器链 (FilterChain)
-
多个过滤器可以串联成一个链。
-
在
doFilter方法中,调用chain.doFilter(request, response)将控制权交给链中的下一个过滤器或目标资源。 -
目标资源处理完请求后,响应会逆序 经过过滤器链返回给客户端。因此可以在
chain.doFilter之后 对响应进行处理(如添加头、压缩)。javapublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 前置处理 (请求到达目标资源前) chain.doFilter(request, response); // 传递给下一个过滤器或目标资源 // 后置处理 (目标资源生成响应后) }
11. Servlet 监听器 (Listener)
监听器用于监听 Web 应用中发生的事件(如 ServletContext 创建/销毁、Session 创建/销毁/属性更改、Request 创建/销毁)。
11.1 监听器的概念与作用
- 初始化/清理: 在应用启动/关闭时加载/释放全局资源(如数据库连接池)。
- 会话跟踪: 统计在线用户数、监听会话创建销毁。
- 请求监控: 记录请求生命周期。
- 属性变更: 监听上下文、会话、请求中属性的变化。
11.2 主要监听器接口
ServletContextListener: 监听ServletContext的创建和销毁事件。void contextInitialized(ServletContextEvent sce): 应用启动,ServletContext创建后调用。void contextDestroyed(ServletContextEvent sce): 应用关闭,ServletContext销毁前调用。
HttpSessionListener: 监听HttpSession的创建和销毁事件。void sessionCreated(HttpSessionEvent se): Session 创建后调用。void sessionDestroyed(HttpSessionEvent se): Session 销毁前调用。
ServletRequestListener: 监听ServletRequest的创建和销毁事件。void requestInitialized(ServletRequestEvent sre): 请求开始时调用。void requestDestroyed(ServletRequestEvent sre): 请求结束时调用。
ServletContextAttributeListener: 监听ServletContext中属性的添加、移除、替换事件。HttpSessionAttributeListener: 监听HttpSession中属性的添加、移除、替换事件。ServletRequestAttributeListener: 监听ServletRequest中属性的添加、移除、替换事件。
11.3 配置监听器
web.xml 配置:
XML
<listener>
<listener-class>com.example.MyContextListener</listener-class>
</listener>
注解配置 (@WebListener):
java
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.ServletContextListener;
@WebListener
public class MyContextListener implements ServletContextListener {
// ... contextInitialized, contextDestroyed 方法
}
12. 异步 Servlet
传统 Servlet 模型是阻塞的:一个线程处理一个请求直到响应完成。对于耗时操作(如长查询、外部服务调用),线程会被长时间占用,影响服务器吞吐量。
12.1 传统 Servlet 的阻塞模型限制
- 线程池中的线程可能被耗尽。
- 高并发下性能下降。
12.2 异步处理支持
Servlet 3.0+ 引入了异步处理。通过注解或 web.xml 声明 Servlet 支持异步:
- 注解:
@WebServlet(asyncSupported = true) - web.xml:
<servlet><async-supported>true</async-supported></servlet>
12.3 AsyncContext 接口
核心对象是 AsyncContext,它代表一个异步操作上下文。
- 启动异步: 在
doXxx方法中调用AsyncContext startAsync(ServletRequest request, ServletResponse response)。这告诉容器当前线程可以释放,异步操作将在另一个线程中完成。 - 完成异步: 在异步操作完成后,调用
asyncContext.complete()通知容器请求处理完成。 - 分发: 也可以调用
asyncContext.dispatch(String path)将请求分发到另一个资源(Servlet/JSP)去生成响应,类似于RequestDispatcher,但发生在异步线程中。
12.4 使用场景
- 长轮询 (Long Polling): 客户端发起请求,服务器在有新数据时才响应。
- 服务器推送 (Server Push): 服务器主动向客户端推送数据。
- 长时间计算: 将耗时计算交给后台线程,主线程立即返回。
基本模式:
java
@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
// 1. 启动异步上下文
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(30000); // 设置超时时间(毫秒)
// 2. 将耗时任务提交给另一个线程池或Executor
executor.submit(() -> {
try {
// 模拟耗时操作
Thread.sleep(5000);
// 3. 在异步线程中获取响应对象并写入响应
HttpServletResponse asyncResponse = (HttpServletResponse) asyncContext.getResponse();
asyncResponse.setContentType("text/plain");
asyncResponse.getWriter().write("Async Response after 5 seconds");
} catch (Exception e) {
// 处理异常
} finally {
// 4. 必须调用 complete() 通知容器请求完成
asyncContext.complete();
}
});
// doGet 方法立即返回,原始线程释放
}
}
13. Servlet 与 Spring Boot
Spring Boot 极大地简化了基于 Servlet 的 Web 应用的开发,但它底层仍然运行在 Servlet 容器(如 Tomcat)之上。
13.1 Spring Boot 对 Servlet 的支持
- 嵌入式容器: Spring Boot 默认将 Servlet 容器(Tomcat, Jetty, Undertow)作为应用的一部分嵌入,无需单独安装和部署 WAR 文件。
spring-boot-starter-web依赖包含了 Tomcat 和 Spring MVC。 - 自动配置: Spring Boot 自动配置 Servlet 容器、DispatcherServlet、字符编码过滤器等。
13.2 自动配置 Servlet 组件
Spring Boot 提供了 RegistrationBean 来方便地注册 Servlet、Filter、Listener:
ServletRegistrationBean: 注册自定义 Servlet。FilterRegistrationBean: 注册自定义 Filter。ServletListenerRegistrationBean: 注册自定义 Listener。
13.3 在 Spring Boot 中定义 Servlet/Filter/Listener
方法一:使用 @ServletComponentScan + @WebServlet/@WebFilter/@WebListener
-
在主应用类上添加
@ServletComponentScan:java@SpringBootApplication @ServletComponentScan // 扫描 @WebServlet, @WebFilter, @WebListener public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } -
在 Servlet/Filter/Listener 类上使用相应的注解:
java@WebServlet(urlPatterns = "/custom") public class CustomServlet extends HttpServlet { ... } @WebFilter(urlPatterns = "/*") public class CustomFilter implements Filter { ... } @WebListener public class CustomListener implements ServletContextListener { ... }
方法二:使用 @Bean + RegistrationBean (更灵活,可控制顺序)
java
@Configuration
public class ServletConfig {
@Bean
public ServletRegistrationBean<CustomServlet> customServletRegistration() {
ServletRegistrationBean<CustomServlet> registrationBean = new ServletRegistrationBean<>();
registrationBean.setServlet(new CustomServlet());
registrationBean.addUrlMappings("/custom");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
@Bean
public FilterRegistrationBean<CustomFilter> customFilterRegistration() {
FilterRegistrationBean<CustomFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CustomFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 设置过滤器顺序
return registrationBean;
}
@Bean
public ServletListenerRegistrationBean<CustomListener> customListenerRegistration() {
return new ServletListenerRegistrationBean<>(new CustomListener());
}
}
13.4 Spring MVC DispatcherServlet 与 Servlet 的关系
- Spring MVC 的核心是
DispatcherServlet,它是一个标准的HttpServlet。 - 它充当前端控制器 (Front Controller),接收所有匹配其映射的请求。
- 它负责将请求路由到合适的控制器 (
@Controller)、处理器 (@RestController),处理视图解析、异常处理等。 - 在 Spring Boot 中,
DispatcherServlet被自动注册,映射路径默认为/(根路径)。可以通过spring.mvc.servlet.path属性修改。
13.5 在 Spring Boot 中访问 Servlet API
在 Spring MVC 的控制器 (@Controller, @RestController) 中,可以直接将 HttpServletRequest, HttpServletResponse, HttpSession 作为方法参数注入:
java
@RestController
public class MyController {
@GetMapping("/info")
public String getRequestInfo(HttpServletRequest request, HttpServletResponse response) {
String userAgent = request.getHeader("User-Agent");
// ... 使用 request, response
return "Info: " + userAgent;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, HttpSession session) {
// ... 验证逻辑
session.setAttribute("user", authenticatedUser);
return ResponseEntity.ok().build();
}
}
也可以通过 ServletRequestAttributes 和 RequestContextHolder 在非控制器类中获取:
java
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attributes.getRequest();
HttpServletResponse response = attributes.getResponse();
14. Servlet 最佳实践
14.1 线程安全注意事项
- 避免使用实例变量: Servlet 实例通常单例 ,多个请求并发访问
service/doXxx方法。实例变量会被所有请求共享,导致数据错乱。不要在 Servlet 中使用实例变量存储请求特定状态! - 使用局部变量: 请求特定的数据应存储在方法局部变量或请求对象 (
request,session) 中。 - 同步谨慎: 如果必须使用共享资源(如计数器),使用同步机制 (
synchronized),但会影响性能。
14.2 资源管理与关闭流
-
在
finally块中关闭InputStream,OutputStream,Reader,Writer, 数据库Connection,Statement,ResultSet等资源。 -
使用 try-with-resources (Java 7+) 自动关闭资源:
javatry (PrintWriter out = response.getWriter()) { out.println("Hello World"); } // 自动关闭 out
14.3 异常处理
-
使用
try-catch捕获并处理预期异常。 -
对于未捕获的异常,容器通常会生成一个包含堆栈跟踪的错误页面(不友好)。
-
可以配置错误页面 (
web.xml):XML<error-page> <exception-type>java.lang.Exception</exception-type> <location>/error.jsp</location> </error-page> <error-page> <error-code>404</error-code> <location>/notfound.html</location> </error-page> -
在 Servlet 中,使用
response.sendError()发送特定错误状态码和消息。
14.4 性能考虑
- 避免同步: 如非必要,避免使用
synchronized。 - 使用连接池: 数据库连接、HTTP 客户端连接等使用连接池管理。
- 缓存: 对频繁访问且变化不频繁的数据使用缓存。
- 异步处理: 对于耗时操作,考虑使用异步 Servlet。
- 优化数据库查询: 使用索引,避免 N+1 查询。
14.5 安全考虑
- 防止跨站脚本攻击 (XSS): 对用户输入进行输出编码,避免直接将用户输入输出到 HTML/JS 中。使用库如 OWASP Java Encoder。
- 防止跨站请求伪造 (CSRF): 使用框架提供的 CSRF 保护机制(如 Spring Security),或生成并验证 Token。
- 防止 SQL 注入: 使用
PreparedStatement进行数据库操作,永远不要拼接 SQL 字符串。 - 输入验证: 验证所有用户输入(类型、长度、格式、范围)。
- 敏感数据处理: 密码等敏感数据应加盐哈希存储,避免明文。
- HTTPS: 使用 HTTPS 加密传输数据。
- 最小权限原则: 应用程序连接数据库等资源时使用最小必要权限的账户。
- 安全配置: 确保服务器、框架的安全配置正确(如禁用不安全的 HTTP 方法)。
15. 实战案例
案例一:基础用户登录与会话管理
功能: 用户通过表单提交用户名和密码,验证成功后创建 Session 记录登录状态,访问受保护页面需要登录。
java
// LoginServlet.java
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
// 简单的内存用户存储 (实际应用中应从数据库获取)
private Map<String, String> validUsers = new HashMap<>();
{
validUsers.put("admin", "admin123"); // 弱密码仅作示例,实际应用需加盐哈希
validUsers.put("user", "userpass");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 显示登录表单
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h2>用户登录</h2>");
out.println("<form method='POST'>");
out.println("用户名: <input type='text' name='username'><br>");
out.println("密码: <input type='password' name='password'><br>");
out.println("<input type='submit' value='登录'>");
out.println("</form>");
out.println("</body></html>");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
// 验证用户名和密码
if (validUsers.containsKey(username) && validUsers.get(username).equals(password)) {
// 登录成功,创建 Session
HttpSession session = request.getSession();
session.setAttribute("user", username);
// 重定向到欢迎页面
response.sendRedirect("welcome");
} else {
// 登录失败
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h2>登录失败!用户名或密码错误。</h2>");
out.println("<a href='login'>返回登录</a>");
out.println("</body></html>");
}
}
}
java
// WelcomeServlet.java (受保护资源)
@WebServlet("/welcome")
public class WelcomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 检查用户是否登录 (Session 中是否有 user)
HttpSession session = request.getSession(false); // 不创建新session
if (session == null || session.getAttribute("user") == null) {
// 未登录,重定向到登录页
response.sendRedirect("login");
return;
}
// 用户已登录,显示欢迎信息
String username = (String) session.getAttribute("user");
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h2>欢迎, " + username + "!</h2>");
out.println("<p>这是您的个人主页。</p>");
out.println("<a href='logout'>注销</a>");
out.println("</body></html>");
}
}
java
// LogoutServlet.java
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取 Session (如果存在)
HttpSession session = request.getSession(false);
if (session != null) {
// 使 Session 失效
session.invalidate();
}
// 重定向到登录页
response.sendRedirect("login");
}
}
案例二:文件上传 Servlet
功能: 允许用户上传文件到服务器。
java
@WebServlet("/upload")
@MultipartConfig // 必须的注解,用于处理 multipart/form-data 请求
public class FileUploadServlet extends HttpServlet {
private static final String UPLOAD_DIR = "uploads"; // 上传目录 (相对于应用根目录)
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 确保上传目录存在
String appPath = request.getServletContext().getRealPath("");
String uploadPath = appPath + File.separator + UPLOAD_DIR;
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
// 获取上传的文件部分
Part filePart = request.getPart("file"); // "file" 是表单中文件字段的 name
String fileName = getSubmittedFileName(filePart); // 获取原始文件名
// 防止路径遍历攻击 (确保只有文件名,没有路径)
fileName = new File(fileName).getName();
// 写入文件
String filePath = uploadPath + File.separator + fileName;
filePart.write(filePath);
out.println("<html><body>");
out.println("<h2>文件上传成功!</h2>");
out.println("<p>文件名: " + fileName + "</p>");
out.println("<p>保存路径: " + filePath + "</p>");
out.println("<a href='upload.html'>继续上传</a>"); // 假设有一个上传表单页面
out.println("</body></html>");
} catch (Exception e) {
out.println("<html><body>");
out.println("<h2>上传失败!</h2>");
out.println("<p>错误: " + e.getMessage() + "</p>");
out.println("</body></html>");
e.printStackTrace();
}
}
// 辅助方法:从 Part 中提取原始文件名 (不同容器实现可能不同)
private String getSubmittedFileName(Part part) {
for (String content : part.getHeader("content-disposition").split(";")) {
if (content.trim().startsWith("filename")) {
return content.substring(content.indexOf('=') + 1).trim().replace("\"", "");
}
}
return null;
}
}
对应的上传表单 (upload.html):
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文件上传</title>
</head>
<body>
<h2>上传文件</h2>
<form action="upload" method="POST" enctype="multipart/form-data">
选择文件: <input type="file" name="file"><br>
<input type="submit" value="上传">
</form>
</body>
</html>
案例三:异步 Servlet 实现简单消息推送
功能: 模拟一个长轮询接口,客户端请求后,服务器在 5 秒后返回响应。
java
@WebServlet(urlPatterns = "/longpoll", asyncSupported = true)
public class LongPollingServlet extends HttpServlet {
private ExecutorService executor = Executors.newCachedThreadPool();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
// 启动异步上下文
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(30000); // 设置超时时间 30秒
// 提交一个延迟任务到线程池
executor.submit(() -> {
try {
// 模拟耗时操作 (等待 5 秒)
Thread.sleep(5000);
// 获取异步响应对象
HttpServletResponse asyncResponse = (HttpServletResponse) asyncContext.getResponse();
asyncResponse.setContentType("text/plain");
asyncResponse.getWriter().write("Server response after 5 seconds delay.");
} catch (Exception e) {
// 处理异常
asyncContext.getResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} finally {
// 完成异步操作
asyncContext.complete();
}
});
// doGet 方法立即返回
}
}
案例四:Spring Boot 中自定义 Filter 实现请求日志与耗时统计
功能: 记录每个请求的 URL、方法、客户端 IP、处理耗时。
java
// LoggingFilter.java
public class LoggingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
long startTime = System.currentTimeMillis();
// 记录请求信息
HttpServletRequest httpRequest = (HttpServletRequest) request;
String uri = httpRequest.getRequestURI();
String method = httpRequest.getMethod();
String clientIP = httpRequest.getRemoteAddr();
// 继续处理请求链
chain.doFilter(request, response);
// 计算耗时
long duration = System.currentTimeMillis() - startTime;
// 记录响应信息 (状态码需转换类型)
int status = ((HttpServletResponse) response).getStatus();
logger.info("{} {} from IP: {}, Status: {}, Duration: {} ms", method, uri, clientIP, status, duration);
}
// ... init, destroy 方法 (可选)
}
在 Spring Boot 中注册 Filter (使用 @Bean):
java
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<LoggingFilter> loggingFilter() {
FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LoggingFilter());
registrationBean.addUrlPatterns("/*"); // 过滤所有请求
registrationBean.setOrder(Ordered.LOWEST_PRECEDENCE - 1); // 尽量靠后执行,确保能记录最终状态
return registrationBean;
}
}
这份指南涵盖了 Java Servlet 的核心概念、API、生命周期、配置、高级特性(过滤器、监听器、异步处理)以及在 Spring Boot 中的集成方式,并提供了可直接运行的实战案例。希望它能成为您学习和开发 Servlet 应用的权威参考。
觉得文章不错,请多多支持,关注我!