Article
第一章:Jakarta Servlet 概述
1.1 什么是 Jakarta Servlet
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| Jakarta Servlet | 是运行在 Web 容器(如 Tomcat)中的 Java 服务器端组件,用于处理 HTTP 请求并生成动态响应。它是 Jakarta EE 规范的一部分,定义了标准的请求-响应编程模型。 | 必须由容器管理生命周期;不能直接通过 main 方法运行。 |
| Servlet 容器 | 提供网络服务、请求解码、响应格式化、线程管理及安全控制的运行环境(如 Apache Tomcat、Jetty)。容器负责加载、初始化、调用和销毁 Servlet 实例。 | 所有 Servlet 必须部署在兼容的容器中才能运行。 |
| 核心接口 | 所有 Servlet 必须实现 jakarta.servlet.Servlet 接口,该接口定义了 init()、service()、destroy() 等生命周期方法。 | 实际开发中通常继承 HttpServlet 而非直接实现接口。 |
| 命名空间变更 | 自 Jakarta EE 9 起,原 javax.servlet 包更名为 jakarta.servlet,这是因 Oracle 将 Java EE 移交 Eclipse 基金会后品牌变更所致。 | 使用 Spring Boot 3.0+ 或 Jakarta EE 9+ 项目时必须使用 jakarta.* 包,否则会报类找不到错误。 |
1.2 Jakarta Servlet 与 Java EE / Jakarta EE 的关系
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| Java EE | Java Platform, Enterprise Edition,是 Sun/Oracle 主导的企业级 Java 应用规范集合,包含 Servlet、JSP、EJB、JPA、JMS 等技术。 | Java EE 8 是最后一个由 Oracle 发布的版本。 |
| Jakarta EE | Java EE 移交 Eclipse 基金会后的继任者,自 2017 年起由社区维护,名称变更为 Jakarta EE,包名从 javax 改为 jakarta。 | Jakarta EE 9 = Java EE 8 功能 + 包名变更;Jakarta EE 10+ 引入新特性。 |
| Servlet 在规范中的地位 | Servlet 是 Jakarta EE 最基础、最核心的 Web 层规范,其他框架(如 Spring MVC、JSF)均构建在其之上。 | 即使使用高级框架,底层仍依赖 Servlet API 处理请求。 |
| 版本演进关系 | - Servlet 4.0 → Java EE 8 - Servlet 5.0 → Jakarta EE 8(仅包名变更) - Servlet 6.0 → Jakarta EE 10(新增功能) - Servlet 6.1 → Jakarta EE 10.1(增强 API) | 开发时需确保 Servlet API 版本与容器(如 Tomcat 10+)兼容。 |
1.3 Jakarta Servlet 的核心作用与典型应用场景
| 场景名称 | 作用说明 | 典型应用示例 | 注意事项 |
|---|---|---|---|
| 动态网页生成 | 根据用户请求动态组装 HTML 内容,替代静态页面。可结合 JSP、Thymeleaf 等模板引擎。 | 用户登录后显示个性化首页;商品列表页实时渲染。 | 需设置正确的 Content-Type(如 text/html;charset=UTF-8)。 |
| RESTful API 开发 | 接收 JSON/XML 请求,执行业务逻辑,返回结构化数据(如 JSON),供前端或移动端调用。 | 用户注册接口 /api/register;订单查询接口 /api/orders/{id}。 | 响应头应设为 application/json,并处理 CORS(跨域)问题。 |
| 微服务通信基础 | 作为微服务的 HTTP 入口,接收服务间调用请求,常与 Spring Boot、Quarkus 等框架结合。 | 订单服务调用库存服务的 /inventory/check 接口。 | 需考虑高并发、超时、重试等分布式问题。 |
| 身份认证与授权 | 通过 Filter 或 Servlet 拦截请求,验证 Token 或 Session,实现统一安全控制。 | 登录拦截器;JWT 验证中间件。 | 敏感操作需配合 HTTPS,防止会话劫持。 |
| 文件上传与下载 | 利用 HttpServletRequest.getParts()(Servlet 3.0+)处理 multipart 表单,实现文件接收与分发。 | 用户头像上传;报表导出下载。 | 需配置 @MultipartConfig 注解或 web.xml 中的 <multipart-config>。 |
| 单点登录(SSO)支撑 | 作为 SSO 系统的回调端点,处理认证中心重定向回来的票据(ticket)。 | 企业内部多个系统共享一套登录状态。 | 需严格校验票据有效性,防止伪造攻击。 |
第二章:开发环境搭建
2.1 JDK 与 Maven 环境准备
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 安装 JDK(推荐 JDK 17 或 21) | 1. 访问 Oracle JDK 或 Eclipse Temurin 下载 LTS 版本。 2. 运行安装程序,选择纯英文路径(如 D:\Java\jdk-21)。3. 配置系统环境变量: - 新建 JAVA_HOME = D:\Java\jdk-21- 在 Path 中添加 %JAVA_HOME%\bin。 | 路径中禁止包含空格或中文;Jakarta Servlet 6.0+ 要求 JDK 11+,推荐使用 JDK 17/21。 |
| 验证 JDK 安装 | 打开命令行,执行:java -versionjavac -version应输出对应版本信息(如 openjdk version "21.0.2")。 | 若提示”不是内部或外部命令”,说明 Path 配置错误,需重启终端或系统。 |
| 安装 Maven | 1. 下载 Apache Maven(如 apache-maven-3.9.12-bin.zip)。2. 解压至目录(如 D:\Maven\apache-maven-3.9.12)。3. 配置环境变量: - 新建 MAVEN_HOME = D:\Maven\apache-maven-3.9.12- 在 Path 中添加 %MAVEN_HOME%\bin。 | 不要将 Maven 放在 C 盘或带空格路径;建议使用 3.8+ 版本以兼容 Jakarta EE。 |
| 配置 Maven 本地仓库与镜像 | 1. 在 Maven 根目录新建 repo 文件夹。2. 编辑 conf/settings.xml:- 取消注释 <localRepository> 并设为 <localRepository>D:\Maven\repo</localRepository>- 在 <mirrors> 中添加阿里云镜像:<mirror><id>aliyun</id><mirrorOf>*</mirrorOf><url>https://maven.aliyun.com/repository/public</url></mirror> | 镜像加速依赖下载;避免使用已废弃的 nexus/content/groups/public 地址。 |
| 验证 Maven 安装 | 执行命令:mvn -v应显示 Maven 版本、Java 版本及 MAVEN_HOME 路径。 | 若报错”JAVA_HOME not found”,需确认 JDK 环境变量是否生效。 |
2.2 Web 容器选择(Tomcat、Jetty 等)
| 容器名称 | 用途说明 | 适用场景 | 注意事项 |
|---|---|---|---|
| Apache Tomcat | 最流行的开源 Servlet 容器,支持 Jakarta Servlet、JSP、WebSocket 等规范。 | 企业级 Web 应用、传统 MVC 架构、学习入门。 | Tomcat 10+ 支持 Jakarta Servlet 5.0+(包名为 jakarta.*);Tomcat 9 仅支持 javax.*。 |
| Eclipse Jetty | 轻量级、嵌入式友好、模块化设计的 Web 容器,启动快、内存占用低。 | 微服务、测试环境、嵌入式部署、WebSocket 密集型应用。 | 适合与 Spring Boot 集成;可通过代码直接启动服务器实例。 |
| Undertow | 由 Red Hat 开发,基于 NIO 的高性能容器,支持阻塞与非阻塞 IO 混合模型。 | 高并发 API 网关、低延迟服务、云原生微服务。 | 常用于替换 Spring Boot 默认 Tomcat;单实例 TPS 高于 Tomcat。 |
| 选择建议 | - 学习/通用开发 → Tomcat 10+ - 嵌入式/轻量 → Jetty - 高性能/微服务 → Undertow | 必须确保容器版本与 Jakarta Servlet 规范版本匹配(如 Servlet 6.0 需 Tomcat 10.1+)。 | 切勿混用 javax 与 jakarta 包,否则运行时抛 ClassNotFoundException。 |
2.3 创建第一个 Jakarta Servlet 项目(Maven + WAR)
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 创建 Maven 项目 | 在命令行执行:mvn archetype:generate -DgroupId=com.example -DartifactId=jakarta-demo -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false生成标准 WAR 项目结构。 | 使用 maven-archetype-webapp 可快速生成 web.xml 和目录结构。 |
修改 pom.xml 支持 Jakarta Servlet | 添加以下依赖(以 Servlet 6.0 为例):xml<br/><properties><br/> <maven.compiler.source>17</maven.compiler.source><br/> <maven.compiler.target>17</maven.compiler.target><br/> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><br/></properties><br/><dependencies><br/> <dependency><br/> <groupId>jakarta.servlet</groupId><br/> <artifactId>jakarta.servlet-api</artifactId><br/> <version>6.0.0</version><br/> <scope>provided</scope><br/> </dependency><br/></dependencies><br/>设置打包类型为 <packaging>war</packaging>。 | scope 必须为 provided,因容器已提供实现;版本需与目标容器兼容(如 Tomcat 10.1 对应 Servlet 6.0)。 |
编写 HelloServlet | 在 src/main/java/com/example/HelloServlet.java 中编写:java<br/>package com.example;<br/>import jakarta.servlet.*;<br/>import jakarta.servlet.http.*;<br/>import java.io.*;<br/>public class HelloServlet extends HttpServlet {<br/> protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {<br/> resp.setContentType("text/html;charset=UTF-8");<br/> PrintWriter out = resp.getWriter();<br/> out.println("<h1>Hello, Jakarta Servlet!</h1>");<br/> }<br/>}<br/> | 必须继承 HttpServlet;方法签名需抛出 ServletException 和 IOException。 |
| 配置 Servlet 映射(注解方式) | 在 HelloServlet 类上添加注解:@WebServlet("/hello")无需修改 web.xml(要求 Servlet 3.0+)。 | 若使用 web.xml,需在 <web-app> 中添加 <servlet> 和 <servlet-mapping>,且 metadata-complete="false"。 |
| 打包与部署 | 1. 执行 mvn clean package 生成 target/jakarta-demo.war。2. 将 WAR 文件复制到 Tomcat 的 webapps/ 目录。3. 启动 Tomcat( bin/startup.bat 或 startup.sh)。4. 浏览器访问 http://localhost:8080/jakarta-demo/hello。 | WAR 文件名即上下文路径;若部署失败,检查日志 logs/catalina.out 或 localhost.log。 |
| 验证运行结果 | 页面应显示:<h1>Hello, Jakarta Servlet!</h1> | 若出现 404,检查 URL 路径、Servlet 映射及 Tomcat 是否加载应用成功。 |
第三章:Servlet 核心接口与类
3.1 jakarta.servlet.Servlet 接口详解
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
init | void init(ServletConfig config) throws ServletException | 初始化 Servlet 实例,仅在生命周期开始时调用一次。可通过 config 获取初始化参数或 ServletContext。 | java<br/>@Override<br/>public void init(ServletConfig config) throws ServletException {<br/> String dbUrl = config.getInitParameter("dbUrl");<br/> System.out.println("DB URL: " + dbUrl);<br/>}<br/> | 若抛出 ServletException,容器将标记该 Servlet 不可用;通常调用 super.init(config) 以保存配置引用。 |
getServletConfig | ServletConfig getServletConfig() | 返回当前 Servlet 的配置对象(由容器在 init 中传入)。 | java<br/>ServletConfig cfg = getServletConfig();<br/>String param = cfg.getInitParameter("timeout");<br/> | 在 init 调用前调用可能返回 null;GenericServlet 已实现此方法并缓存配置。 |
service | void service(ServletRequest req, ServletResponse res) throws ServletException, IOException | 处理客户端请求的核心方法,每次 HTTP 请求都会触发。 | java<br/>@Override<br/>public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {<br/> res.getWriter().write("Handled by custom service");<br/>}<br/> | 开发者通常不直接重写此方法,而是继承 HttpServlet 并重写 doGet/doPost 等。 |
getServletInfo | String getServletInfo() | 返回关于 Servlet 的描述信息(如作者、版本、版权等)。 | java<br/>@Override<br/>public String getServletInfo() {<br/> return "HelloServlet v1.0 by Example Inc.";<br/>}<br/> | 默认返回空字符串;常用于管理或监控工具展示元数据。 |
destroy | void destroy() | 在 Servlet 被销毁前释放资源(如关闭数据库连接、清理缓存),仅调用一次。 | java<br/>@Override<br/>public void destroy() {<br/> if (dbConnection != null) dbConnection.close();<br/> System.out.println("Servlet destroyed");<br/>}<br/> | 容器保证在所有请求线程结束后才调用;不可在此方法中启动新线程。 |
💡 说明:所有 Jakarta EE 9+ 项目必须使用
jakarta.servlet.Servlet,而非旧版javax.servlet.Servlet。
3.2 GenericServlet 抽象类
| 方法/特性 | 语法/说明 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 继承关系 | public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable | 提供与协议无关的 Servlet 基础实现,简化开发。 | public class MyServlet extends GenericServlet { ... } | 适用于非 HTTP 协议场景(极少使用);Web 开发应优先使用 HttpServlet。 |
service(抽象) | public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException | 必须由子类实现,用于处理请求。 | java<br/>@Override<br/>public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {<br/> res.setContentType("text/plain");<br/> res.getWriter().println("Generic response");<br/>}<br/> | 是唯一必须重写的方法;其他生命周期方法已有默认空实现。 |
log | public void log(String msg)public void log(String message, Throwable t) | 向容器日志系统记录消息或异常。 | java<br/>log("User accessed generic servlet");<br/>log("Error occurred", e);<br/> | 日志格式和输出位置由容器决定(如 Tomcat 的 catalina.out)。 |
getInitParameter | public String getInitParameter(String name) | 直接获取初始化参数,无需先获取 ServletConfig。 | String timeout = getInitParameter("timeout"); | 内部调用 getServletConfig().getInitParameter(name),若未初始化则抛 NullPointerException。 |
getServletContext | public ServletContext getServletContext() | 获取 Web 应用上下文对象。 | java<br/>ServletContext ctx = getServletContext();<br/>String appPath = ctx.getContextPath();<br/> | 仅在 init 调用后可用;可用于共享应用级数据。 |
⚠️ 注意:
GenericServlet不处理 HTTP 方法分发,无法区分 GET/POST,因此不适用于标准 Web 开发。
3.3 HttpServlet 抽象类及其常用方法
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
doGet | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException | 处理 HTTP GET 请求,常用于查询、页面展示等幂等操作。 | java<br/>@Override<br/>protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {<br/> resp.setContentType("text/html;charset=UTF-8");<br/> resp.getWriter().println("<h1>Welcome</h1>");<br/>}<br/> | 浏览器地址栏访问、超链接点击均触发此方法;避免在此执行修改数据的操作。 |
doPost | protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException | 处理 HTTP POST 请求,常用于表单提交、文件上传等非幂等操作。 | java<br/>@Override<br/>protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {<br/> String username = req.getParameter("user");<br/> resp.getWriter().println("Hello " + username);<br/>}<br/> | 请求体可携带大量数据;需处理中文乱码(建议在 Filter 中统一设置 req.setCharacterEncoding("UTF-8"))。 |
doPut | protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException | 处理 HTTP PUT 请求,常用于更新资源(RESTful 风格)。 | java<br/>@Override<br/>protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {<br/> resp.setStatus(HttpServletResponse.SC_OK);<br/>}<br/> | 需前端显式发送 PUT 请求(如 AJAX 或 Postman);浏览器表单不支持 PUT。 |
doDelete | protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException | 处理 HTTP DELETE 请求,常用于删除资源(RESTful 风格)。 | java<br/>@Override<br/>protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {<br/> String id = req.getParameter("id");<br/> // delete resource by id<br/> resp.setStatus(HttpServletResponse.SC_NO_CONTENT);<br/>}<br/> | 同 PUT,需程序化发起请求;响应通常返回 204(无内容)。 |
service(已实现) | protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException | 根据 HTTP 方法自动分发到 doGet、doPost 等方法。 | 无需重写;内部逻辑:if (method.equals("GET")) doGet(...);else if (method.equals("POST")) doPost(...); | 若重写此方法,将覆盖自动分发机制,需自行处理方法路由。 |
doHead / doOptions / doTrace | 对应 HTTP 方法的处理方法 | 处理 HEAD、OPTIONS、TRACE 请求(较少使用)。 | 通常保留默认实现;doHead 默认调用 doGet 但不输出 body。 | TRACE 方法存在安全风险(XST 攻击),生产环境应禁用。 |
✅ 最佳实践:
- 实际开发中几乎总是继承
HttpServlet。- 重写
doGet和doPost即可满足 95% 以上需求。- 若需支持多种方法,可在
doPost中调用doGet(req, resp)实现复用。
第四章:请求与响应处理
4.1 HttpServletRequest 常用方法
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
getMethod | String getMethod() | 获取 HTTP 请求方法(如 GET、POST)。 | String method = req.getMethod(); // "GET" | 常用于判断请求类型以执行不同逻辑。 |
getRequestURI | String getRequestURI() | 获取请求的 URI(不含协议、主机、端口和查询字符串)。 | String uri = req.getRequestURI(); // "/app/hello" | 不包含上下文路径以外的协议信息。 |
getRequestURL | StringBuffer getRequestURL() | 获取完整的请求 URL(含协议、主机、端口、路径)。 | String url = req.getRequestURL().toString(); // "http://localhost:8080/app/hello" | 返回 StringBuffer,可追加修改。 |
getContextPath | String getContextPath() | 获取 Web 应用的上下文路径(部署名)。 | String ctx = req.getContextPath(); // "/myapp" | 根应用返回空字符串 "",非 /。 |
getHeader | String getHeader(String name) | 获取指定请求头的值(如 User-Agent、Referer)。 | String ua = req.getHeader("User-Agent"); | 头名不区分大小写;若不存在返回 null。 |
getHeaders | Enumeration<String> getHeaders(String name) | 获取同名请求头的所有值(适用于重复头字段)。 | Enumeration<String> cookies = req.getHeaders("Cookie"); | 少数头(如 Cookie)可能多次出现。 |
getHeaderNames | Enumeration<String> getHeaderNames() | 获取所有请求头名称的枚举。 | java<br/>Enumeration<String> names = req.getHeaderNames();<br/>while (names.hasMoreElements()) {<br/> String h = names.nextElement();<br/>}<br/> | 用于调试或日志记录。 |
getRemoteAddr | String getRemoteAddr() | 获取客户端 IP 地址。 | String ip = req.getRemoteAddr(); // "127.0.0.1" | 若经过代理,需检查 X-Forwarded-For 头获取真实 IP。 |
getProtocol | String getProtocol() | 获取请求使用的协议及版本。 | String proto = req.getProtocol(); // "HTTP/1.1" | 可用于判断是否支持 HTTP/2(但 Servlet 6.0+ 才有明确支持)。 |
getSession | HttpSession getSession()HttpSession getSession(boolean create) | 获取当前会话对象,若不存在则创建(或根据参数决定)。 | java<br/>HttpSession session = req.getSession();<br/>session.setAttribute("user", "alice");<br/> | 默认 create=true;高并发下注意会话管理开销。 |
4.2 HttpServletResponse 常用方法
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
setStatus | void setStatus(int sc) | 设置 HTTP 响应状态码(如 200、302、404)。 | resp.setStatus(404); | 仅用于成功或重定向类状态;错误状态建议用 sendError。 |
sendError | void sendError(int sc)void sendError(int sc, String msg) | 发送错误响应(自动设置状态码并生成错误页面)。 | resp.sendError(404, "Page not found"); | 调用后响应即结束,不可再写入 body;容器可能覆盖自定义消息。 |
setHeader | void setHeader(String name, String value) | 设置响应头(覆盖同名头)。 | resp.setHeader("Cache-Control", "no-cache"); | 常用于控制缓存、CORS、文件下载等。 |
addHeader | void addHeader(String name, String value) | 添加响应头(保留已有同名头)。 | java<br/>resp.addHeader("X-Custom", "value1");<br/>resp.addHeader("X-Custom", "value2");<br/> | 适用于允许多值的头(如 Set-Cookie)。 |
setIntHeader / addIntHeader | void setIntHeader(String name, int value)void addIntHeader(String name, int value) | 设置整数值的响应头。 | resp.setIntHeader("Retry-After", 3600); | 自动转换为字符串;避免手动拼接数字。 |
setContentType | void setContentType(String type) | 设置响应内容的 MIME 类型和字符编码。 | java<br/>resp.setContentType("text/html;charset=UTF-8");<br/>resp.setContentType("application/json");<br/> | 必须在 getWriter() 前调用,否则无效。 |
setCharacterEncoding | void setCharacterEncoding(String charset) | 单独设置响应字符编码(优先级高于 setContentType 中的编码)。 | resp.setCharacterEncoding("UTF-8"); | 同样需在 getWriter() 前设置。 |
getWriter | PrintWriter getWriter() | 获取字符输出流,用于发送文本响应(如 HTML、JSON)。 | java<br/>PrintWriter out = resp.getWriter();<br/>out.println("<h1>Hello</h1>");<br/> | 与 getOutputStream() 互斥,不可同时使用。 |
getOutputStream | ServletOutputStream getOutputStream() | 获取字节输出流,用于发送二进制数据(如图片、文件)。 | java<br/>ServletOutputStream os = resp.getOutputStream();<br/>os.write(fileBytes);<br/> | 适用于文件下载、PDF 生成等场景。 |
sendRedirect | void sendRedirect(String location) | 发送 302 重定向响应,跳转到新 URL。 | java<br/>resp.sendRedirect("/login.jsp");<br/>resp.sendRedirect("https://example.com");<br/> | URL 可为相对或绝对;浏览器地址栏会改变。 |
4.3 请求参数获取与响应内容输出
| 操作类型 | 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|---|
| 获取单值参数 | getParameter | String getParameter(String name) | 获取指定名称的请求参数值(取第一个)。 | String user = req.getParameter("username"); | 若参数不存在返回 null;适用于普通表单字段。 |
| 获取多值参数 | getParameterValues | String[] getParameterValues(String name) | 获取同名参数的所有值(如复选框)。 | String[] hobbies = req.getParameterValues("hobby"); | 若无此参数返回 null;若有单值则数组长度为 1。 |
| 获取所有参数名 | getParameterNames | Enumeration<String> getParameterNames() | 枚举所有参数名称。 | Enumeration<String> names = req.getParameterNames(); | 用于动态遍历参数。 |
| 获取参数 Map | getParameterMap | Map<String, String[]> getParameterMap() | 获取所有参数的键值对(值为字符串数组)。 | java<br/>Map<String, String[]> map = req.getParameterMap();<br/>for (String key : map.keySet()) { ... }<br/> | 只读 Map,不可修改;常用于日志或验证。 |
| 输出文本响应 | getWriter + println | PrintWriter out = resp.getWriter(); out.println(content); | 向客户端发送 HTML、JSON 等文本内容。 | java<br/>resp.setContentType("application/json");<br/>PrintWriter out = resp.getWriter();<br/>out.print("{\"status\":\"ok\"}");<br/> | 必须先设 Content-Type 和编码。 |
| 输出二进制响应 | getOutputStream + write | ServletOutputStream os = resp.getOutputStream(); os.write(bytes); | 发送图片、PDF、ZIP 等二进制文件。 | java<br/>resp.setContentType("image/png");<br/>resp.setContentLength(imageBytes.length);<br/>os.write(imageBytes);<br/> | 需设置 Content-Length 以优化传输。 |
4.4 字符编码与中文乱码处理
| 问题场景 | 解决方案 | 操作细节 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| POST 请求中文乱码 | 在读取参数前设置请求编码 | 调用 req.setCharacterEncoding("UTF-8") | java<br/>@Override<br/>protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {<br/> req.setCharacterEncoding("UTF-8");<br/> String name = req.getParameter("name"); // 正常中文<br/>}<br/> | 必须在 getParameter() 前调用;仅对 POST 有效(因 GET 参数由 URL 编码决定)。 |
| GET 请求中文乱码 | 修改 Tomcat 配置或手动解码 | 方式一:在 server.xml 的 <Connector> 中添加 URIEncoding="UTF-8"方式二:手动重新编码: new String(value.getBytes("ISO-8859-1"), "UTF-8") | java<br/>String raw = req.getParameter("q");<br/>String decoded = new String(raw.getBytes("ISO-8859-1"), "UTF-8");<br/> | 推荐使用方式一;方式二易出错且不通用。 |
| 响应中文乱码 | 设置响应内容类型和编码 | 调用 resp.setContentType("text/html;charset=UTF-8") 或分开设置 | java<br/>resp.setContentType("text/html;charset=UTF-8");<br/>// 或<br/>resp.setCharacterEncoding("UTF-8");<br/>resp.setHeader("Content-Type", "text/html");<br/> | 必须在 getWriter() 前设置;否则浏览器按默认编码(如 ISO-8859-1)解析。 |
| 统一编码 Filter | 创建全局字符编码过滤器 | 实现 Filter,在 doFilter 中统一设置请求/响应编码 | java<br/>public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {<br/> req.setCharacterEncoding("UTF-8");<br/> res.setCharacterEncoding("UTF-8");<br/> chain.doFilter(req, res);<br/>}<br/> | 最佳实践:通过 web.xml 或 @WebFilter("/*") 注册,避免每个 Servlet 重复设置。 |
✅ 总结:
- POST 乱码 →
req.setCharacterEncoding("UTF-8")- GET 乱码 → 配置 Tomcat
URIEncoding="UTF-8"- 响应乱码 →
resp.setContentType("...;charset=UTF-8")- 终极方案 → 使用全局 Filter 统一处理
第五章:Servlet 生命周期与线程模型
5.1 Servlet 的生命周期(init, service, destroy)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
init | public void init(ServletConfig config) throws ServletException | 初始化 Servlet 实例,仅在生命周期开始时调用一次。用于加载配置、建立数据库连接等一次性操作。 | java<br/>@Override<br/>public void init(ServletConfig config) throws ServletException {<br/> String dbUrl = config.getInitParameter("dbUrl");<br/> try {<br/> Class.forName("com.mysql.cj.jdbc.Driver");<br/> connection = DriverManager.getConnection(dbUrl, "user", "pass");<br/> System.out.println("Database connected in init()");<br/> } catch (Exception e) {<br/> throw new ServletException("Failed to initialize DB", e);<br/> }<br/>}<br/> | 必须调用 super.init(config) 或重写无参 init() 方法以避免 config 丢失;若抛出 ServletException,容器将不启用该 Servlet。 |
service | public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException | 处理客户端请求的核心方法。每次 HTTP 请求都会触发,由容器分配新线程执行。 | // 通常不直接重写此方法// HttpServlet 已实现自动分发至 doGet/doPost 等 | 开发者应继承 HttpServlet 并重写 doGet/doPost;直接重写需自行解析 HTTP 方法。 |
destroy | public void destroy() | 在 Servlet 被销毁前释放资源,仅调用一次。用于关闭连接、停止线程、持久化数据等。 | java<br/>@Override<br/>public void destroy() {<br/> if (connection != null) {<br/> try {<br/> connection.close();<br/> System.out.println("Database connection closed");<br/> } catch (SQLException e) {<br/> e.printStackTrace();<br/> }<br/> }<br/>}<br/> | 容器保证在所有请求处理完成后才调用;异常终止(如 kill -9)不会触发此方法。 |
💡 生命周期流程: 容器加载类 → 反射创建实例 → 调用
init()→ 多次调用service()(多线程)→ 应用关闭时调用destroy()→ 对象被 GC 回收。
5.2 单例模式与多线程安全问题
| 概念/问题 | 说明 | 风险示例 | 安全实践 | 注意事项 |
|---|---|---|---|---|
| 单例模式 | 每个 Servlet 类在容器中仅有一个实例,所有请求共享该实例。 | java<br/>public class UnsafeServlet extends HttpServlet {<br/> private int counter = 0;<br/> protected void doGet(HttpServletRequest req, HttpServletResponse resp) {<br/> counter++; // 非线程安全<br/> resp.getWriter().println("Count: " + counter);<br/> }<br/>}<br/> | 使用局部变量、只读成员、同步块或线程安全集合。 | 实例变量(成员变量)在多线程下存在竞态条件,应避免使用。 |
| 线程安全原则 | 所有请求共用同一个 Servlet 实例,但每个请求拥有独立的 HttpServletRequest 和 HttpServletResponse 对象。 | 成员变量被多个线程同时修改导致数据错乱。 | - 业务逻辑使用方法内局部变量 - 共享资源使用 synchronized 或 java.util.concurrent 工具- 使用 ThreadLocal 存储线程私有数据 | HttpServletRequest/HttpServletResponse 本身是线程安全的,因其为每个请求新建。 |
| 安全示例 | 正确使用局部变量和外部服务 | java<br/>public class SafeServlet extends HttpServlet {<br/> private final UserService userService = new UserService();<br/> protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {<br/> String userId = req.getParameter("id");<br/> User user = userService.findById(userId); // 假设 userService 是无状态或线程安全的<br/> resp.getWriter().println("User: " + user.getName());<br/> }<br/>}<br/> | 将状态封装在无状态服务中;避免在 Servlet 中维护用户会话状态(应使用 HttpSession)。 | 若必须使用可变成员,需加锁,但会降低并发性能。 |
5.3 异步处理支持(Servlet 3.0+)
| 方法/特性 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 启用异步支持 | @WebServlet(urlPatterns = "/async", asyncSupported = true) | 声明 Servlet 支持异步处理。 | @WebServlet(urlPatterns = "/long-task", asyncSupported = true)public class AsyncServlet extends HttpServlet { ... } | 必须显式设置 asyncSupported = true,否则调用 startAsync() 会抛 IllegalStateException。 |
| 启动异步上下文 | AsyncContext startAsync(HttpServletRequest req, HttpServletResponse resp) | 暂挂当前请求处理,释放容器线程,后续在其他线程完成响应。 | java<br/>@Override<br/>protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {<br/> AsyncContext asyncCtx = req.startAsync();<br/> asyncCtx.setTimeout(10000); // 10秒超时<br/> executor.submit(() -> {<br/> try {<br/> // 模拟耗时操作<br/> Thread.sleep(5000);<br/> asyncCtx.getResponse().getWriter().println("Async task done");<br/> asyncCtx.complete();<br/> } catch (Exception e) {<br/> asyncCtx.complete();<br/> }<br/> });<br/>}<br/> | 必须调用 asyncCtx.complete() 结束响应;超时未完成会触发 timeout 事件。 |
| 设置超时回调 | void addTimeoutListener(Runnable listener) | 注册异步操作超时时的回调。 | asyncCtx.addTimeoutListener(() -> { System.out.println("Async operation timed out");}); | 超时后容器可能自动 complete,需在 listener 中清理资源。 |
| 分派回同步处理 | void dispatch(String path) | 将异步请求转发给另一个 Servlet 或 JSP 进行最终渲染。 | asyncCtx.dispatch("/result.jsp"); | 常用于异步计算完成后跳转到结果页面。 |
| 获取原始请求/响应 | HttpServletRequest getRequest()HttpServletResponse getResponse() | 从 AsyncContext 中获取原始请求和响应对象。 | java<br/>HttpServletRequest req = asyncCtx.getRequest();<br/>String param = req.getParameter("data");<br/> | 可在异步线程中安全使用这些对象写入响应。 |
✅ 异步适用场景:
- 调用外部慢速服务(如 REST API、数据库查询)
- 长轮询(Long Polling)或 Server-Sent Events(SSE)
- 提高容器线程利用率,避免阻塞 I/O 占用工作线程
⚠️ 注意:
- 异步处理不能提升单个请求的速度,但能提升系统吞吐量。
- 必须使用线程池(如
Executors.newCachedThreadPool())管理后台任务,避免创建过多线程。
第六章:Servlet 配置方式
6.1 使用 web.xml 进行传统配置
| 配置元素 | 语法结构 | 用途 | 示例代码 | 注意事项 |
|---|---|---|---|---|
<servlet> 声明 | xml<br/><servlet><br/> <servlet-name>名称</servlet-name><br/> <servlet-class>全限定类名</servlet-class><br/> <init-param><br/> <param-name>参数名</param-name><br/> <param-value>参数值</param-value><br/> </init-param><br/> <load-on-startup>整数</load-on-startup><br/></servlet><br/> | 注册 Servlet 类并设置初始化参数和加载顺序。 | xml<br/>HelloServlet<br/>com.example.HelloServlet<br/>greeting<br/>Welcome<br/>1<br/> | servlet-name 必须唯一;load-on-startup 值越小优先级越高,负数或省略表示首次请求时加载。 |
<servlet-mapping> 映射 | xml<br/><servlet-mapping><br/> <servlet-name>已声明的名称</servlet-name><br/> <url-pattern>URL 模式</url-pattern><br/></servlet-mapping><br/> | 将 Servlet 绑定到一个或多个 URL 路径。 | xml<br/>HelloServlet<br/>/hello<br/>*.do<br/> | 支持多 <url-pattern>;模式类型包括:精确匹配(/hello)、路径匹配(/api/*)、扩展名匹配(*.do)。 |
<filter> 声明 | xml<br/><filter><br/> <filter-name>名称</filter-name><br/> <filter-class>全限定类名</filter-class><br/> <init-param>...</init-param><br/></filter><br/> | 注册 Filter 类。 | xml<br/>EncodingFilter<br/>com.example.EncodingFilter<br/>...<br/> | 与 Servlet 声明结构类似,但使用 <filter> 标签。 |
<filter-mapping> 映射 | xml<br/><filter-mapping><br/> <filter-name>名称</filter-name><br/> <url-pattern>/*</url-pattern><br/></filter-mapping><br/> | 指定 Filter 应用的 URL 范围。 | xml<br/>EncodingFilter<br/>/*<br/> | 可使用 <servlet-name> 替代 <url-pattern> 以仅过滤特定 Servlet。 |
<listener> 声明 | xml<br/><listener><br/> <listener-class>全限定监听器类名</listener-class><br/></listener><br/> | 注册监听器(如上下文、会话生命周期事件)。 | xml<br/>com.example.AppContextListener<br/> | 监听器类需实现 ServletContextListener、HttpSessionListener 等接口。 |
metadata-complete 属性 | <web-app metadata-complete="true"> | 若设为 true,容器将忽略所有注解配置,仅使用 web.xml。 | xml<br/><web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"<br/> version="4.0"<br/> metadata-complete="true"><br/> | 默认为 false;若使用注解,必须设为 false 或省略。 |
6.2 使用注解(@WebServlet, @WebFilter, @WebListener)
| 注解名称 | 语法 | 用途 | 示例代码 | 注意事项 |
|---|---|---|---|---|
@WebServlet | java<br/>@WebServlet(<br/> name = "名称",<br/> urlPatterns = {"/path", "*.ext"},<br/> initParams = {<br/> @WebInitParam(name = "key", value = "val")<br/> },<br/> loadOnStartup = 1,<br/> asyncSupported = true<br/>)<br/> | 替代 web.xml 中的 <servlet> 和 <servlet-mapping>。 | java<br/>@WebServlet(<br/> name = "HelloServlet",<br/> urlPatterns = "/hello",<br/> initParams = {<br/> @WebInitParam(name = "greeting", value = "Hi")<br/> },<br/> loadOnStartup = 2<br/>)<br/>public class HelloServlet extends HttpServlet { ... }<br/> | value 与 urlPatterns 互斥;若未指定 name,默认为全类名;需确保容器扫描该类(Tomcat 自动扫描,Spring Boot 需 @ServletComponentScan)。 |
@WebFilter | java<br/>@WebFilter(<br/> filterName = "名称",<br/> urlPatterns = "/*",<br/> servletNames = {"Servlet1"},<br/> dispatcherTypes = {DispatcherType.REQUEST}<br/>)<br/> | 替代 web.xml 中的 <filter> 和 <filter-mapping>。 | java<br/>@WebFilter(urlPatterns = "/*")<br/>public class EncodingFilter implements Filter {<br/> public void doFilter(...) { ... }<br/>}<br/> | urlPatterns、servletNames、value 可组合使用;dispatcherTypes 控制过滤的请求类型(REQUEST/FORWARD/ERROR 等)。 |
@WebListener | @WebListener | 标记监听器类,自动注册。 | java<br/>@WebListener<br/>public class AppContextListener implements ServletContextListener {<br/> public void contextInitialized(ServletContextEvent sce) { ... }<br/>}<br/> | 无需参数;容器根据实现的接口自动识别监听事件类型。 |
| 启用注解扫描(Spring Boot) | @ServletComponentScan(basePackages = "com.example") | 在 Spring Boot 中启用对 @WebServlet 等注解的扫描。 | java<br/>@SpringBootApplication<br/>@ServletComponentScan("com.example")<br/>public class Application {<br/> public static void main(String[] args) {<br/> SpringApplication.run(Application.class, args);<br/> }<br/>}<br/> | 非 Spring Boot 项目(如纯 Tomcat)无需此注解,容器自动处理。 |
6.3 动态注册 Servlet 与 Filter(编程式配置)
| 操作类型 | 方法/接口 | 语法 | 用途 | 示例代码 | 注意事项 |
|---|---|---|---|---|---|
实现 ServletContainerInitializer | public interface ServletContainerInitializer { void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;} | 在应用启动时动态注册组件。 | java<br/>public class MyServletInitializer implements ServletContainerInitializer {<br/> public void onStartup(Set<Class<?>> c, ServletContext ctx) {<br/> ServletRegistration.Dynamic reg = ctx.addServlet("DynamicServlet", DynamicServlet.class);<br/> reg.addMapping("/dynamic");<br/> reg.setLoadOnStartup(1);<br/> }<br/>}<br/> | 需在 META-INF/services/jakarta.servlet.ServletContainerInitializer 文件中声明实现类全名。 | |
使用 ServletContext.addServlet | ServletRegistration.Dynamic addServlet(String name, Class<? extends Servlet> servletClass) | 动态添加 Servlet。 | java<br/>ServletContext ctx = ...;<br/>ServletRegistration.Dynamic reg = ctx.addServlet("ReportServlet", ReportServlet.class);<br/>reg.setAsyncSupported(true);<br/>reg.addMapping("/report/*");<br/> | 返回 ServletRegistration.Dynamic 对象,可进一步配置 init 参数、映射等。 | |
使用 ServletContext.addFilter | FilterRegistration.Dynamic addFilter(String name, Class<? extends Filter> filterClass) | 动态添加 Filter。 | java<br/>FilterRegistration.Dynamic freg = ctx.addFilter("AuthFilter", AuthFilter.class);<br/>freg.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/api/*");<br/> | addMappingForUrlPatterns 支持通配符;第二个参数 isMatchAfter 控制过滤器链顺序。 | |
在 Spring Boot 中使用 RegistrationBean | @Bean | 通过 Spring Bean 方式注册。 | java<br/>@Bean<br/>public ServletRegistrationBean myServlet() {<br/> return new ServletRegistrationBean<>(new MyServlet(), "/custom/*");<br/>}<br/>@Bean<br/>public FilterRegistrationBean encodingFilter() {<br/> FilterRegistrationBean bean = new FilterRegistrationBean<>();<br/> bean.setFilter(new EncodingFilter());<br/> bean.addUrlPatterns("/*");<br/> bean.setOrder(1);<br/> return bean;<br/>}<br/> | setOrder() 控制 Filter 执行顺序(数值越小越先执行);适用于需要依赖注入的场景。 | |
| 获取并修改现有注册 | ServletRegistration getServletRegistration(String name)FilterRegistration getFilterRegistration(String name) | 查询已注册的组件并修改其配置。 | java<br/>ServletRegistration reg = ctx.getServletRegistration("default");<br/>if (reg != null) {<br/> reg.addMapping("/fallback/*");<br/>}<br/> | 通常用于扩展现有 Servlet(如 default Servlet)的功能。 |
✅ 配置方式对比:
web.xml:集中管理,适合大型团队或遗留系统。- 注解:简洁直观,适合现代轻量级开发。
- 编程式:高度灵活,适用于框架集成、条件注册或运行时动态配置。
第七章:高级特性
7.1 过滤器(Filter)机制
| 方法/概念 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
doFilter | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException | 拦截请求和响应,执行预处理或后处理逻辑。 | java<br/>public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {<br/> HttpServletRequest request = (HttpServletRequest) req;<br/> System.out.println("Before processing: " + request.getRequestURI());<br/> chain.doFilter(req, res); // 放行<br/> System.out.println("After processing");<br/>}<br/> | 必须调用 chain.doFilter() 否则请求中断;可修改请求/响应对象(如包装)。 |
init | public void init(FilterConfig filterConfig) throws ServletException | 初始化 Filter,获取配置参数。 | java<br/>public void init(FilterConfig config) throws ServletException {<br/> this.encoding = config.getInitParameter("encoding");<br/>}<br/> | 仅调用一次;可通过 filterConfig.getServletContext() 获取上下文。 |
destroy | public void destroy() | 释放 Filter 资源。 | public void destroy() { System.out.println("Filter destroyed"); } | 应用关闭时调用一次。 |
FilterChain | void doFilter(ServletRequest request, ServletResponse response) | 将请求传递给下一个 Filter 或目标 Servlet。 | chain.doFilter(request, response); | 若不调用,后续组件不会执行;可在放行前后添加逻辑。 |
| 拦截路径匹配 | urlPatterns 支持:- 精确路径 /login- 目录匹配 /api/*- 扩展名匹配 *.jsp- 全局匹配 /* | 控制 Filter 应用范围。 | @WebFilter(urlPatterns = "/admin/*")public class AuthFilter implements Filter { ... } | /* 匹配所有请求(包括静态资源);/ 不是有效模式。 |
| 过滤器链顺序 | web.xml 中 <filter-mapping> 的声明顺序决定执行顺序;注解方式按类名字母排序(不可靠)。 | 控制多个 Filter 的执行次序。 | 在 web.xml 中先声明 A 再声明 B,则 A 先执行(请求阶段),B 先执行(响应阶段)。 | 响应阶段的执行顺序与请求阶段相反。 |
7.2 监听器(Listener)机制
| 监听器接口 | 回调方法 | 触发时机 | 示例代码 | 注意事项 |
|---|---|---|---|---|
ServletContextListener | contextInitialized(ServletContextEvent sce)contextDestroyed(ServletContextEvent sce) | Web 应用启动/关闭时触发。 | java<br/>public void contextInitialized(ServletContextEvent sce) {<br/> System.out.println("App started");<br/> sce.getServletContext().setAttribute("startTime", System.currentTimeMillis());<br/>}<br/> | 用于初始化全局资源(如数据库连接池、缓存)。 |
HttpSessionListener | sessionCreated(HttpSessionEvent se)sessionDestroyed(HttpSessionEvent se) | 会话创建/销毁时触发。 | java<br/>public void sessionCreated(HttpSessionEvent se) {<br/> System.out.println("New session: " + se.getSession().getId());<br/>}<br/> | 可用于统计在线用户数。 |
HttpSessionAttributeListener | attributeAdded(HttpSessionBindingEvent e)attributeRemoved(...)attributeReplaced(...) | 会话属性变更时触发。 | java<br/>public void attributeAdded(HttpSessionBindingEvent e) {<br/> if ("user".equals(e.getName())) {<br/> System.out.println("User logged in: " + e.getValue());<br/> }<br/>}<br/> | 可监控登录状态变化。 |
ServletRequestListener | requestInitialized(ServletRequestEvent sre)requestDestroyed(ServletRequestEvent sre) | 每个 HTTP 请求开始/结束时触发。 | java<br/>public void requestInitialized(ServletRequestEvent sre) {<br/> HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();<br/> System.out.println("Request from: " + req.getRemoteAddr());<br/>}<br/> | 可用于请求日志、性能监控。 |
| 注册方式 | @WebListener 或 web.xml <listener> | 声明监听器生效。 | java<br/>@WebListener<br/>public class AppListener implements ServletContextListener { ... }<br/> | 容器自动识别实现的接口类型。 |
7.3 会话管理(HttpSession)
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
getSession | HttpSession getSession()HttpSession getSession(boolean create) | 获取当前会话,若不存在则创建(或根据参数决定)。 | java<br/>HttpSession session = req.getSession();<br/>session.setAttribute("user", currentUser);<br/> | 默认 create=true;getSession(false) 用于检查是否已登录。 |
setAttribute | void setAttribute(String name, Object value) | 存储会话属性。 | session.setAttribute("cart", new ShoppingCart()); | 值必须可序列化(若需集群复制)。 |
getAttribute | Object getAttribute(String name) | 获取会话属性。 | User user = (User) session.getAttribute("user"); | 返回 Object,需强制类型转换。 |
removeAttribute | void removeAttribute(String name) | 移除指定属性。 | session.removeAttribute("tempData"); | 不会销毁整个会话。 |
invalidate | void invalidate() | 立即销毁会话及所有属性。 | session.invalidate(); // 用户登出 | 会话 ID 失效,客户端下次请求将创建新会话。 |
setMaxInactiveInterval | void setMaxInactiveInterval(int interval) | 设置会话超时时间(秒)。 | session.setMaxInactiveInterval(1800); // 30分钟 | 覆盖 web.xml 中的全局设置;0 或负数表示永不过期(不推荐)。 |
getId | String getId() | 获取会话唯一标识(JSESSIONID)。 | String id = session.getId(); | 用于调试或日志追踪。 |
isNew | boolean isNew() | 判断会话是否为本次请求新建。 | java<br/>if (session.isNew()) {<br/> resp.getWriter().println("Welcome first-time visitor!");<br/>}<br/> | 首次访问且无 JSESSIONID Cookie 时返回 true。 |
💡 会话存储机制:
- 服务端:内存中存储
HttpSession对象- 客户端:通过 Cookie(
JSESSIONID=xxx)或 URL 重写传递 ID- 安全建议:生产环境启用 HTTPS,防止会话劫持
7.4 文件上传与下载处理
| 功能 | 方法/配置 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|---|
| 启用上传支持 | @MultipartConfig 或 web.xml <multipart-config> | java<br/>@MultipartConfig(<br/> fileSizeThreshold = 1024 * 1024,<br/> maxFileSize = 1024 * 1024 * 5,<br/> maxRequestSize = 1024 * 1024 * 10<br/>)<br/> | 声明 Servlet 支持 multipart/form-data 请求。 | java<br/>@WebServlet("/upload")<br/>@MultipartConfig(maxFileSize = 5_000_000)<br/>public class UploadServlet extends HttpServlet { ... }<br/> | 必须添加,否则 getParts() 抛异常。 |
| 获取上传文件 | Part getPart(String name) | Part filePart = req.getPart("file"); | 获取单个上传文件部件。 | java<br/>Part filePart = req.getPart("avatar");<br/>String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString();<br/>filePart.write("/uploads/" + fileName);<br/> | getSubmittedFileName() 可能为 null(需校验);write() 保存到磁盘。 |
| 获取所有部件 | Collection<Part> getParts() | Collection<Part> parts = req.getParts(); | 遍历所有表单字段和文件。 | java<br/>for (Part part : req.getParts()) {<br/> if (part.getContentType() != null) {<br/> // 是文件<br/> } else {<br/> // 是普通参数<br/> }<br/>}<br/> | 普通参数可通过 part.getInputStream() 读取值。 |
| 文件下载响应 | 设置响应头 + 输出流 | resp.setContentType("application/octet-stream");resp.setHeader("Content-Disposition", "attachment; filename=report.pdf"); | 强制浏览器下载文件而非打开。 | java<br/>resp.setHeader("Content-Length", String.valueOf(file.length()));<br/>try (InputStream in = Files.newInputStream(path)) {<br/> in.transferTo(resp.getOutputStream());<br/>}<br/> | 必须设置 Content-Disposition: attachment;中文文件名需 URL 编码(如 URLEncoder.encode(name, "UTF-8"))。 |
| 流式下载大文件 | 使用缓冲区避免内存溢出 | byte[] buffer = new byte[4096];int bytesRead;while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead);} | 安全传输大文件。 | java<br/>try (InputStream in = new FileInputStream(file);<br/> OutputStream out = resp.getOutputStream()) {<br/> byte[] buf = new byte[8192];<br/> int n;<br/> while ((n = in.read(buf)) >= 0) {<br/> out.write(buf, 0, n);<br/> }<br/>}<br/> | 避免一次性加载整个文件到内存。 |
✅ 安全建议:
- 上传文件需校验扩展名、MIME 类型、内容特征(防木马)
- 下载路径需校验,防止目录遍历(如
../../../etc/passwd)
第八章:Jakarta Servlet 新特性(6.0+)
8.1 包名变更:从 javax.servlet 到 jakarta.servlet
| 变更项 | 说明 | 迁移操作 | 示例(旧 → 新) | 注意事项 |
|---|---|---|---|---|
| 命名空间 | 因 Oracle 将 Java EE 移交 Eclipse 基金会,所有 API 包名从 javax.* 改为 jakarta.*。 | 1. 全局替换代码中 import javax.servlet 为 import jakarta.servlet2. 更新 Maven/Gradle 依赖 3. 修改 web.xml 命名空间 | import javax.servlet.http.HttpServlet;→ import jakarta.servlet.http.HttpServlet; | 不兼容:javax.servlet.Servlet 与 jakarta.servlet.Servlet 是两个不同类,混用会导致 ClassCastException 或 ClassNotFoundException。 |
| Maven 依赖 | Jakarta EE 9+ 使用新坐标。 | 替换依赖: | ||
| 旧坐标 | 新坐标 | |||
groupId: javax.servlet | groupId: jakarta.servlet | |||
artifactId: javax.servlet-api | artifactId: jakarta.servlet-api | |||
version: 4.0.1 | version: 6.0.0 | |||
scope: provided | scope: provided | |||
| 版本对应关系: - Servlet 4.0 → javax(Java EE 8)- Servlet 5.0 → jakarta(Jakarta EE 9,仅包名变)- Servlet 6.0 → jakarta(Jakarta EE 10,含新功能) | ||||
web.xml 命名空间 | XML Schema URI 更新。 | 修改根元素: | xml<br/>旧:<br/><web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"<br/> version="4.0"><br/><br/>新:<br/><web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"<br/> version="6.0"><br/> | version 需匹配 Servlet 规范版本(如 6.0 对应 Jakarta EE 10)。 |
| 容器兼容性 | Tomcat 10+、Jetty 11+、Undertow 2.2+ 支持 jakarta.*。 | 升级 Web 容器: - Tomcat 9 → 仅支持 javax- Tomcat 10.0.x → Jakarta EE 9(Servlet 5.0) - Tomcat 10.1.x → Jakarta EE 10(Servlet 6.0) | 若使用 Spring Boot: - Spring Boot 2.x + Tomcat 9 → javax- Spring Boot 3.x + Tomcat 10.1 → jakarta | 不可混用:项目中所有依赖(包括第三方库如 FileUpload)必须统一为 jakarta 或 javax。 |
8.2 HTTP/2 支持与 Server Push(已弃用)
| 特性 | 说明 | 状态 | 替代方案 | 注意事项 |
|---|---|---|---|---|
| HTTP/2 原生支持 | Servlet 4.0(Java EE 8)首次引入对 HTTP/2 的容器级支持(多路复用、头部压缩等)。 | 保留:Jakarta Servlet 6.0+ 仍支持 HTTP/2,但由容器实现(如 Tomcat 10 启用 SSL 后自动协商)。 | 无需代码修改;确保服务器配置 TLS 1.2+ 并启用 HTTP/2。 | 应用层无需感知;可通过 HttpServletRequest.getProtocol() 查看协议(如 “HTTP/2.0”)。 |
| Server Push | 通过 PushBuilder 主动推送资源(如 CSS/JS),减少往返延迟。 | 已弃用:Servlet 6.0 起标记为 deprecated,Jakarta EE 11(Servlet 6.1)中移除。 | - 使用 <link rel="preload">- 利用 HTTP/1.1 并行请求 - 采用 Early Hints (103) 响应 | 不再推荐使用;现代浏览器对 Server Push 支持差,易导致资源重复加载。 |
PushBuilder 用法(历史参考) | HttpServletRequest.getPushBuilder() 返回构建器。 | 仅适用于 Servlet 4.0–5.0 | java<br/>// Servlet 5.0 示例(已过时)<br/>PushBuilder push = req.getPushBuilder();<br/>if (push != null) {<br/> push.path("/style.css").push();<br/>}<br/> | 在 Servlet 6.0+ 中调用 getPushBuilder() 返回 null。 |
8.3 Servlet 6.1 新增功能
| 新增功能 | 方法/语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 带状态码的重定向 | void sendRedirect(int status, String location) | 允许指定 3xx 状态码(如 301、303),替代默认 302。 | java<br/>// 永久重定向<br/>resp.sendRedirect(301, "/new-page");<br/><br/>// See Other<br/>resp.sendRedirect(303, "/confirmation");<br/> | 支持的状态码必须是 3xx 重定向类;否则抛 IllegalArgumentException。 |
| 错误分派中的查询字符串 | RequestDispatcher.ERROR_QUERY_STRING | 在错误页面中获取原始请求的查询参数。 | java<br/>// error.jsp 或 ErrorServlet 中<br/>String qs = (String) request.getAttribute(RequestDispatcher.ERROR_QUERY_STRING);<br/>System.out.println("Original query: " + qs);<br/> | 属性名为 "jakarta.servlet.error.query_string";仅在 ERROR 分派时存在。 |
| ByteBuffer I/O 支持 | int read(ByteBuffer buffer)void write(ByteBuffer buffer) | 在 ServletInputStream/ServletOutputStream 中直接使用 NIO ByteBuffer,提升 I/O 性能。 | java<br/>ServletInputStream in = req.getInputStream();<br/>ByteBuffer buf = ByteBuffer.allocate(1024);<br/>int n = in.read(buf);<br/><br/>ServletOutputStream out = resp.getOutputStream();<br/>out.write(buf);<br/> | 适用于高并发、非阻塞 I/O 场景;可与虚拟线程(Java 21+)协同优化吞吐量。 |
| Charset 编码设置 | void setCharacterEncoding(Charset charset) | 使用 java.nio.charset.Charset 对象替代字符串编码名,避免拼写错误。 | java<br/>// 推荐方式<br/>resp.setCharacterEncoding(StandardCharsets.UTF_8);<br/><br/>// 旧方式(仍支持)<br/>resp.setCharacterEncoding("UTF-8");<br/> | 提供编译期类型安全;StandardCharsets 包含常用编码(UTF_8、ISO_8859_1 等)。 |
| 最低 Java 版本要求 | Jakarta EE 11(含 Servlet 6.1)要求 Java 17+ | 利用现代 Java 特性(Records、Pattern Matching 等)。 | 可在 Servlet 中安全使用 switch 表达式、var 等。 | 无法在 Java 8/11 上运行 Jakarta EE 11 应用。 |
✅ Servlet 6.1 核心价值:
- 精细化控制:如精确重定向状态码、完整错误上下文
- 现代化 I/O:
ByteBuffer支持为高性能网关、代理提供基础- 类型安全:
Charset参数消除字符串硬编码风险- 云原生就绪:与 Java 17+、虚拟线程、容器化部署深度协同
第九章:部署与调试
9.1 打包为 WAR 文件
| 步骤名称 | 操作细节 | 工具/命令 | 注意事项 |
|---|---|---|---|
| 确保项目结构合规 | 项目必须包含 WEB-INF/web.xml(可选)和 WEB-INF/classes/ 目录存放编译后的类文件。Maven 项目默认结构为:- src/main/java → 编译到 WEB-INF/classes- src/main/webapp → 根目录静态资源 | 使用 Maven Archetype:mvn archetype:generate -DarchetypeArtifactId=maven-archetype-webapp | 若使用注解配置(如 @WebServlet),web.xml 可省略,但需确保 metadata-complete="false"(默认)。 |
| 设置打包类型 | 在 pom.xml 中指定 <packaging>war</packaging> | <project>...<packaging>war</packaging>...</project> | 默认为 jar;若未设置,Maven 不会生成 WAR。 |
| 添加 Servlet API 依赖 | 引入 jakarta.servlet-api,作用域为 provided | xml<br/><dependency><br/> <groupId>jakarta.servlet</groupId><br/> <artifactId>jakarta.servlet-api</artifactId><br/> <version>6.0.0</version><br/> <scope>provided</scope><br/></dependency><br/> | scope=provided 表示容器已提供实现,避免打包进 WAR 导致冲突。 |
| 执行打包命令 | 使用 Maven 构建 | mvn clean package | 成功后在 target/ 目录生成 项目名.war 文件;大小通常几 MB 到几十 MB。 |
| 验证 WAR 内容 | 解压 WAR 文件检查结构 | jar -tf target/myapp.war或用解压软件打开 | 必须包含: - WEB-INF/classes/com/example/HelloServlet.class- META-INF/MANIFEST.MF- 静态资源(如 index.html)位于根目录 |
9.2 部署到 Tomcat / Jetty
| 容器 | 部署方式 | 操作步骤 | 访问路径 | 注意事项 |
|---|---|---|---|---|
| Apache Tomcat | 手动拷贝 WAR | 1. 启动 Tomcat(bin/startup.sh 或 .bat)2. 将 WAR 文件复制到 webapps/ 目录3. Tomcat 自动解压并部署 | http://localhost:8080/项目名/路径(如 http://localhost:8080/jakarta-demo/hello) | - WAR 文件名即上下文路径 - 若命名为 ROOT.war,则路径为 /- 查看日志: logs/localhost.<date>.log |
| Apache Tomcat | Context 配置(推荐生产) | 1. 在 conf/Catalina/localhost/ 下创建 myapp.xml2. 内容: <Context docBase="/path/to/app" /> | http://localhost:8080/myapp/... | - docBase 可指向未打包的目录或 WAR 文件- 文件名决定上下文路径 - 不修改 server.xml,更安全 |
| Eclipse Jetty | 命令行部署 | 1. 将 WAR 放入 Jetty 的 webapps/ 目录2. 启动 Jetty: java -jar start.jar | http://localhost:8080/项目名/路径 | Jetty 默认端口 8080;可通过 jetty.port=8081 修改 |
| Eclipse Jetty | 嵌入式部署(开发) | 在 Java 代码中启动:java<br/>Server server = new Server(8080);<br/>WebAppContext webapp = new WebAppContext();<br/>webapp.setWar("target/myapp.war");<br/>server.setHandler(webapp);<br/>server.start();<br/> | 同上 | 适用于单元测试或快速原型验证 |
| 通用验证 | 检查部署状态 | - Tomcat Manager 页面(需配置用户) - 查看 webapps/ 下是否生成同名文件夹 | 若部署成功,webapps/ 下应有 myapp/ 目录 | 首次部署可能较慢(因解压和初始化) |
9.3 常见错误排查(404、500、版本不兼容等)
| 错误现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| HTTP 404 Not Found | 1. 上下文路径错误 2. Servlet 映射未生效 3. WAR 未正确部署 | 1. 检查 URL 是否为 http://host:port/项目名/servlet路径2. 查看 webapps/ 下是否有解压目录3. 检查 web.xml 或注解中的 urlPatterns | - 确认 WAR 文件名(如 demo.war → /demo)- 若使用注解,确保类被扫描(非 Spring Boot 需在 web.xml 设 metadata-complete="false") |
| HTTP 500 Internal Error | 1. Servlet 类找不到 2. 初始化异常 3. 代码逻辑错误 | 1. 查看容器日志(Tomcat: logs/catalina.out)2. 搜索 ClassNotFoundException 或 ServletException | - 确认依赖未打包进 WAR(如 Servlet API 应为 provided)- 检查 init() 中是否有异常- 修复业务代码中的空指针等错误 |
ClassNotFoundException: javax.servlet.Servlet | 混用 javax 与 jakarta 包 | 1. 检查代码 import2. 检查依赖坐标 3. 检查容器版本 | - 代码中统一使用 jakarta.servlet.*- Maven 依赖使用 jakarta.servlet:jakarta.servlet-api- 使用 Tomcat 10+(非 Tomcat 9) |
| Servlet not initialized | load-on-startup 配置错误或 init 抛异常 | 查看日志中是否有 StandardWrapper.Throwable | - 确保 init() 不抛未处理异常- 检查 web.xml 中 load-on-startup 值是否合理 |
| 中文乱码 | 请求/响应编码未设置 | 1. POST 提交乱码 → 未设 req.setCharacterEncoding("UTF-8")2. GET 乱码 → Tomcat 未设 URIEncoding="UTF-8"3. 响应乱码 → 未设 resp.setContentType("...;charset=UTF-8") | - 添加全局 Filter 统一设置编码 - 修改 Tomcat conf/server.xml:<Connector ... URIEncoding="UTF-8" /> |
| 端口冲突 | 8080 被占用 | 执行 `netstat -ano | findstr :8080(Windows)或 lsof -i :8080`(Linux) |
| WAR 部署失败 | 1. 文件损坏 2. 权限不足 3. 磁盘空间不足 | 1. 检查日志中 SEVERE 级别错误 2. 尝试手动解压 WAR 验证完整性 | - 重新执行 mvn package- 确保 Tomcat 对 webapps/ 有写权限- 清理磁盘空间 |
✅ 调试最佳实践:
- 始终查看容器日志(
catalina.out或jetty.log)- 开发阶段使用 IDE 集成部署(如 IntelliJ + Tomcat 插件),支持热加载
- 生产环境采用
context.xml方式部署,避免直接操作webapps/
第十章:最佳实践与生态整合
10.1 与 JSP、EL、JPA 等 Jakarta EE 组件集成
| 组件 | 集成方式 | 用途 | 示例代码 / 配置 | 注意事项 |
|---|---|---|---|---|
| JSP(Jakarta Server Pages) | 在 Servlet 中通过 RequestDispatcher 转发到 JSP 页面 | 实现 MVC 模式:Servlet 处理逻辑,JSP 负责视图渲染 | java<br/>protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {<br/> req.setAttribute("message", "Hello from Servlet");<br/> req.getRequestDispatcher("/result.jsp").forward(req, resp);<br/>}<br/> | - JSP 文件放在 src/main/webapp/ 下- 避免在 JSP 中写 Java 脚本(使用 EL + JSTL) |
| EL(Expression Language) | 在 JSP 中使用 ${} 表达式访问请求/会话属性 | 简化数据输出,替代 <%= %> 脚本 | <p>Welcome, ${user.name}!</p><c:if test="${not empty cart.items}">...</c:if> | 需引入 JSTL:xml<br/><dependency><br/> <groupId>org.glassfish.web</groupId><br/> <artifactId>jakarta.servlet.jsp.jstl</artifactId><br/></dependency><br/> |
| JPA(Jakarta Persistence API) | 在 Servlet 中注入或创建 EntityManager 操作数据库 | 实现数据持久化,替代 JDBC 手动编码 | java<br/>@WebServlet("/users")<br/>public class UserServlet extends HttpServlet {<br/> protected void doGet(...) {<br/> EntityManagerFactory emf = ...; // 通常由 CDI 或手动管理<br/> EntityManager em = emf.createEntityManager();<br/> List<User> users = em.createQuery("SELECT u FROM User u", User.class).getResultList();<br/> req.setAttribute("users", users);<br/> req.getRequestDispatcher("/list.jsp").forward(req, resp);<br/> }<br/>}<br/> | - 纯 Servlet 项目需手动管理 EntityManager 生命周期- 推荐结合 CDI(如 Weld)实现依赖注入 - 生产环境应使用连接池(如 HikariCP) |
| CDI(Contexts and Dependency Injection) | 使用 @Inject 注入 Bean 到 Servlet | 实现控制反转,解耦业务逻辑 | java<br/>@WebServlet("/api")<br/>public class ApiService extends HttpServlet {<br/> @Inject<br/> private UserService userService;<br/><br/> protected void doGet(...) {<br/> User u = userService.findById("123");<br/> }<br/>}<br/> | - 需添加 beans.xml(空文件即可)到 WEB-INF/- 依赖 Jakarta EE 容器(如 WildFly、Payara)或嵌入 Weld |
| 统一异常处理 | 使用 web.xml 的 <error-page> 或全局 Filter | 集中处理 404、500 等错误 | xml<br/><error-page><br/> <error-code>500</error-code><br/> <location>/error.jsp</location><br/></error-page><br/> | 错误页面可通过 request.getAttribute("jakarta.servlet.error.exception") 获取异常对象 |
10.2 与 Spring Boot / Spring MVC 的关系
| 对比维度 | Jakarta Servlet 原生 | Spring Boot / Spring MVC | 说明 |
|---|---|---|---|
| 编程模型 | 继承 HttpServlet,重写 doGet/doPost | 使用 @RestController + @GetMapping 等注解 | Spring 提供更简洁的声明式 API |
| 依赖注入 | 需 Jakarta EE 容器 + CDI(如 @Inject) | 内置 IoC 容器,支持 @Autowired、@Service 等 | Spring 的 DI 更成熟、灵活 |
| 配置方式 | web.xml、@WebServlet、动态注册 | application.properties + 注解自动配置 | Spring Boot “约定优于配置”大幅减少样板代码 |
| JPA 集成 | 手动管理 EntityManager 或依赖 CDI | Spring Data JPA:继承 JpaRepository 即可获得 CRUD 方法 | Spring Data JPA 极大简化数据访问层 |
| 内嵌服务器 | 需外部部署(Tomcat/Jetty) | 内置 Tomcat/Jetty/Undertow,java -jar app.jar 直接运行 | Spring Boot 应用是可执行 JAR,非 WAR |
| 适用场景 | 轻量级 Web 应用、学习底层原理、受限环境 | 企业级应用、快速开发、微服务 | Spring MVC 底层仍基于 Servlet(DispatcherServlet 是核心) |
| 迁移路径 | 可逐步引入 Spring:将 Servlet 逻辑移至 @Controller | 不建议从 Spring 回退到原生 Servlet | 若已使用 Spring Boot,无需直接编写 HttpServlet |
💡 关键结论:
- Spring MVC 是 Servlet 的高级封装,其
DispatcherServlet作为前端控制器,将请求路由到@Controller方法。- 学习原生 Servlet 有助于理解 Spring 底层机制(如请求生命周期、Filter 链)。
- 新项目推荐 Spring Boot;遗留系统维护或教学场景可使用原生 Servlet。
10.3 微服务与云原生下的 Servlet 应用
| 场景 | 挑战 | 解决方案 | 工具/框架 | 注意事项 |
|---|---|---|---|---|
| 单体架构 → 微服务 | Servlet 应用通常为单体,难以拆分 | 1. 将业务模块重构为独立服务 2. 使用 REST API 通信(Servlet 作为 API 网关或后端服务) | - Spring Boot + Spring Cloud - Quarkus(轻量级 Jakarta EE) | 避免在 Servlet 中直接调用其他服务数据库;采用 API 合约 |
| 容器化部署 | WAR 部署模式不适合 Docker | 1. 改造为可执行 JAR(如 Spring Boot) 2. 或使用 Tomcat 官方镜像部署 WAR | Dockerfile 示例:dockerfile<br/>FROM tomcat:10.1-jre17<br/>COPY myapp.war /usr/local/tomcat/webapps/<br/>EXPOSE 8080<br/> | - 镜像大小:Tomcat 基础镜像约 200–300MB - 推荐多阶段构建减小体积 |
| 无状态化 | HttpSession 依赖内存存储,不适用于水平扩展 | 1. 禁用会话(RESTful 无状态) 2. 或将会话外置到 Redis | 使用 Spring Session + Redis:@EnableRedisHttpSession | 原生 Servlet 可通过自定义 HttpSession 存储实现,但复杂度高 |
| 健康检查与可观测性 | 缺乏标准监控端点 | 1. 添加 /health、/metrics 端点2. 集成 Micrometer + Prometheus | 在 Servlet 中实现:java<br/>@WebServlet("/health")<br/>public class HealthServlet extends HttpServlet {<br/> protected void doGet(...) {<br/> resp.getWriter().print("{\"status\":\"UP\"}");<br/> }<br/>}<br/> | 云原生平台(K8s)依赖 /health 进行存活/就绪探针 |
| 启动速度与内存占用 | 传统 Servlet 容器启动慢、内存大 | 采用 GraalVM 原生镜像或轻量级运行时 | - Quarkus(支持 Jakarta Servlet) - Helidon - Spring Native(实验性) | Tomcat + WAR 启动通常需数秒;Quarkus 可达毫秒级 |
| 服务发现与配置中心 | 静态配置无法适应动态环境 | 外部化配置 + 服务注册 | - Spring Cloud Config - Consul/Etcd + 自定义初始化 Listener | 可在 ServletContextListener.contextInitialized() 中加载远程配置 |
✅ 云原生最佳实践:
- 优先选择 JAR 而非 WAR:便于容器化、CI/CD 流水线
- 无状态设计:避免
HttpSession,使用 JWT 或 OAuth2 令牌- 标准化运维接口:提供
/health、/info、/metrics- 轻量化运行时:考虑 Quarkus 或 Spring Boot AOT 编译