S01-03 Servlet-核心API
[TOC]
ServletConfig
是 Servlet 规范中的核心接口之一。当 Servlet 容器(如 Apache Tomcat)初始化一个 Servlet 时,会为该 Servlet 创建一个唯一的 javax.servlet.ServletConfigServletConfig 对象。
- 一对一生命周期绑定:每个 Servlet 实例对应一个专属的
ServletConfig对象,各个 Servlet 之间的配置相互独立、互不干扰。 - 主要职责:向 Servlet 传递部署时的初始化参数(Init Parameters),提供当前 Servlet 的注册名称,并作为当前 Servlet 访问全局
ServletContext的入口桥梁。
配置方式
XML 配置方式
在传统的 web.xml 部署描述符中,初始化参数定义在 <servlet> 标签内部的 <init-param> 子标签中:
<servlet>
<servlet-name>OrderServlet</servlet-name>
<servlet-class>com.example.OrderServlet</servlet-class>
<init-param>
<param-name>defaultPageSize</param-name>
<param-value>20</param-value>
</init-param>
<init-param>
<param-name>charset</param-name>
<param-value>UTF-8</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>OrderServlet</servlet-name>
<url-pattern>/order</url-pattern>
</servlet-mapping>注解配置方式
在 Servlet 3.0 及更高规范中,可以通过 @WebServlet 注解配合 @WebInitParam 直接在类声明上配置参数:
// 使用注解声明 Servlet 元数据与专属初始化参数
@WebServlet(
name = "OrderServlet",
urlPatterns = "/order",
// 配置当前 Servlet 专有的初始化参数列表
initParams = {
@WebInitParam(name = "defaultPageSize", value = "20"),
@WebInitParam(name = "charset", value = "UTF-8")
}
)
public class OrderServlet extends HttpServlet {}获取与使用
获取与使用 :ServletConfig 主要有两种方式
在实际开发中,自定义的 Servlet 通常继承自 HttpServlet(间接继承自 GenericServlet)。
- 显式调用:通过
this.getServletConfig()获取配置对象后再调用各方法。 - 隐式委托:
GenericServlet实现了ServletConfig接口,可以直接在子类中使用this.getInitParameter("key"),内部会自动委托给ServletConfig执行。
import javax.servlet.ServletConfig;
// 继承 HttpServlet,直接调用父类封装的配置方法
public class OrderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 方式一:从当前 Servlet 实例中提取配置对象
ServletConfig config = getServletConfig();
String pageSize = config.getInitParameter("defaultPageSize");
ServletContext context = config.getServletContext();
}
}初始化流程
Servlet 容器从解析配置到完成注入经历以下步骤:
解析配置:容器在启动时或首次请求到达时,扫描
web.xml或@WebServlet注解,提取指定 Servlet 的元数据及<init-param>键值对。实例化包装对象:容器内部为该 Servlet 创建具体的配置实例(如 Tomcat 中的
StandardWrapper与其外观对象StandardWrapperFacade)。反射实例化 Servlet:容器利用反射机制调用 Servlet 的无参构造函数创建其实例。
注入配置:容器调用
Servlet.init(ServletConfig config)方法,将装填好参数的配置对象作为实参传入。缓存引用:
GenericServlet的init(ServletConfig)方法将传入的引用存入私有字段this.config,并调用无参的init()供子类重写扩展。此处的this.config被transient修饰,表示它只能存在于内存中,无法被序列化,保证了它的安全性。响应服务:后续进入
service()及doGet()/doPost()处理阶段时,Servlet 实例随时读取该配置。
容器内部架构
在 Servlet 容器底层实现中,为了保证安全性和隔离性,通常采用外观模式(Facade Pattern) 来包装真实配置对象。
Tomcat 内部核心控制器为 StandardWrapper,它包含了 Servlet 的完整底层运行上下文;但在向 Servlet 传递时,容器会将其包装为 StandardWrapperFacade。这样既实现了 ServletConfig 接口,又对业务代码隐藏了底层容器私有属性和敏感方法。
与 ServletContext 对比
两者虽然都用于存储配置或提供环境支持,但作用域与设计意图截然不同:
| 维度 | ServletConfig | ServletContext |
|---|---|---|
| 作用范围 | 单个 Servlet 私有 | 整个 Web 应用全局共享 |
| 实例数量 | 多个(每个 Servlet 映射一个) | 唯一(每个 Web 应用只有一个) |
| 配置来源 | <servlet> 下的 <init-param> | <context-param> |
| 共享属性操作 | 不支持 setAttribute | 支持 setAttribute / getAttribute |
| 设计用途 | 当前 Servlet 的专属参数(如默认分页、独立编码) | 全局公共配置(如全局日志、全局缓存、公共资源路径) |
API: ServletConfig
StringgetInitParameter():(String name),读取指定初始化参数。根据参数名获取预设的字符串配置值。若指定名称不存在则返回null。Enumeration<String>getInitParameterNames():(),获取所有初始化参数名称。返回当前 Servlet 配置的所有初始化参数名称枚举集合。若未配置任何参数,则返回一个空枚举,绝不返回null。StringgetServletName():(),获取 Servlet 注册名称。返回在部署描述符(web.xml)中声明的<servlet-name>,或@WebServlet(name = ...)中指定的逻辑名称。ServletContextgetServletContext():(),获取全局上下文引用。返回当前 Web 应用程序唯一的 ServletContext 实例,作为访问跨组件资源、共享属性以及容器底层能力的桥梁。
注意事项:
- 只读不可变设计:
ServletConfig接口未提供任何类似setInitParameter的修改方法。所有参数均属于启动期静态元数据,运行期间不可动态增删或修改。- 类型擦除与空值防御:参数值全部以
String形式存储。在进行数值类型转换(如Integer.parseInt()、Boolean.parseBoolean())时,必须进行非空检查与NumberFormatException异常防御,避免因配置格式错误导致 Servlet 启动失败或请求中断。- 与 Context 参数的界限:
ServletConfig.getInitParameter()仅能读取<init-param>或@WebInitParam声明的局部参数,无法读取web.xml中根节点下的<context-param>全局参数(后者需由ServletContext.getInitParameter()读取)。- 构造方法调用禁区:在 Servlet 的构造方法中严禁调用
getServletConfig()或getServletContext()。此时容器仅完成了对象的反射实例化,尚未调用init(ServletConfig)进行配置注入,过早调用必将抛出NullPointerException。init(ServletConfig)覆写陷阱:若在自定义类中覆写了带参的init(ServletConfig config)方法,必须首先显式调用super.init(config)。一旦遗漏父类调用,GenericServlet内部的this.config引用将保持为null,后续所有委派方法均会抛出空指针异常。生产环境强烈建议仅重写无参的init()模板方法。
package com.example.servlet.config.api;
import java.io.IOException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class NamingAndContextApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
ServletConfig config = getServletConfig();
// 1. 遍历当前配置持有的所有初始化参数名
Enumeration<String> names = config.getInitParameterNames();
while (names.hasMoreElements()) {
String paramName = names.nextElement();
// 2. 根据参数名精准读取参数取值
String paramValue = config.getInitParameter(paramName);
writer.println(paramName + " -> " + paramValue);
}
// 3. 读取当前 Servlet 的逻辑注册别名
String servletName = config.getServletName();
// 4. 获取所属 Web 应用的全局上下文
ServletContext servletContext = config.getServletContext();
String contextPath = servletContext.getContextPath();
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().write(String.format("Servlet 注册别名: %s, 归属上下文路径: %s", servletName, contextPath));
}
}ServletContext
是 Java Servlet 规范中定义的全局接口,代表当前 Web 应用程序的运行上下文环境。javax.servlet.ServletContext
- 单一实例:每一个部署到 Servlet 容器(如 Apache Tomcat)中的 Web 应用,在同一个 JVM 内只对应一个唯一的
ServletContext实例。 - 全局共享:作为应用级别的顶级容器,当前 Web 应用内的所有 Servlet、Filter、Listener 均可访问同一个
ServletContext。 - 环境门面:它不仅负责在不同组件间共享数据,还充当了业务代码与底质容器通信的标准化入口。
生命周期
ServletContext 对象的存在周期与 Web 应用程序的生命周期完全一致:
容器启动与解析:Servlet 容器启动并加载 Web 应用程序时,解析
web.xml或相关注解配置。上下文初始化:容器实例化该应用的
ServletContext对象,并读取全局初始化参数。通知监听器:容器触发
ServletContextListener.contextInitialized()事件,执行系统启动任务(如框架初始化、数据库连接池预热)。服务运行期:在整个应用对外提供服务期间,该对象持续存活于内存中供各组件读取和修改。
容器停止与清理:Web 应用被卸载或容器优雅停机时,容器触发
ServletContextListener.contextDestroyed()方法执行资源释放。对象销毁:监听器执行完毕后,容器销毁
ServletContext实例并交由垃圾回收器回收。
获取途径
在不同的组件与处理阶段中,有多种途径可以获取到 ServletContext 实例:
// 途径一:直接通过继承的 HttpServlet 获取
ServletContext ctx1 = getServletContext();
// 途径二:通过当前请求对象获取
ServletContext ctx2 = request.getServletContext();
// 途径三:通过会话对象获取
ServletContext ctx3 = session.getServletContext();- 基于 ServletConfig:通过
servletConfig.getServletContext()获取(GenericServlet已对其进行封装,子类可直接调用getServletContext())。 - 基于 HttpServletRequest:Servlet 3.0 新增了
request.getServletContext(),在未持有 Servlet 实例的工具类中更为便捷。 - 基于 HttpSession:调用
session.getServletContext()。 - 基于 FilterConfig:在过滤器中通过
filterConfig.getServletContext()获取。 - 基于 ServletContextEvent:在应用级监听器中通过
event.getServletContext()获取。
全局参数
全局初始化参数针对整个 Web 应用生效,通常用于声明与业务组件解耦的全局配置(如外部配置文件路径、全局调试开关)。
在 web.xml 中使用 <context-param> 标签进行配置:
<context-param>
<param-name>globalConfigPath</param-name>
<param-value>/WEB-INF/system-config.properties</param-value>
</context-param>在代码中通过以下方法读取:
getInitParameter(String name):根据参数键名获取对应的字符串值;若键不存在则返回null。getInitParameterNames():获取包含所有全局初始化参数名称的Enumeration<String>。
域对象操作
ServletContext 是 Servlet 规范中生命周期最长、作用域最大的域对象(Application Scope)。
// 向应用域中写入全局共享数据
context.setAttribute("globalVisitorCount", 100);
// 从应用域中读取指定键名的共享数据
Object count = context.getAttribute("globalVisitorCount");
// 从应用域中显式移除不再需要的键值数据
context.removeAttribute("globalVisitorCount");- 线程安全提示:由于所有客户端请求线程共享同一个
ServletContext,多线程并发读写存入的属性时必须采取并发同步策略(例如使用AtomicInteger、ConcurrentHashMap或显式锁机制),否则会引发数据不一致问题。 - 内存占用考量:存入
ServletContext的对象在整个 Web 应用生命周期内不会被自动回收,应避免在此存放大量或高频变动的数据,以防止内存溢出。
资源读取
Web 应用打包为 WAR 包后,传统的相对路径无法直接对应操作系统底层的文件路径。ServletContext 提供了统一的资源定位与读取支持:
// 获取 Web 应用根目录下资源的绝对磁盘物理路径
String realPath = context.getRealPath("/WEB-INF/config.properties");
// 直接以输入流的方式读取应用根目录下的静态资源
InputStream in = context.getResourceAsStream("/WEB-INF/db.properties");
// 根据资源文件后缀名获取对应的标准 MIME 类型
String mimeType = context.getMimeType("report.pdf");getRealPath(String path):将相对于 Web 应用根目录的虚拟路径转换为服务器本地磁盘的真实物理绝对路径。需要注意,若 WAR 包未解压直接在内存中运行,该方法可能返回null。getResourceAsStream(String path):以 Web 根目录为基准直接打开输入流,路径必须以/开头。此方式在打包部署环境下具备最佳的移植性。getMimeType(String file):解析文件扩展名对应的互联网媒体类型(如传入logo.png返回image/png)。
动态注册
从 Servlet 3.0 规范开始,ServletContext 支持在 Web 应用初始化阶段动态注册三大核心组件,这使得各类微框架无需在 web.xml 中编写繁复的静态标签:
addServlet(String servletName, Class<? extends Servlet> servletClass):动态注册 Servlet,并返回ServletRegistration.Dynamic对象以进一步绑定 URL 映射和初始化参数。addFilter(String filterName, Class<? extends Filter> filterClass):动态注册 Filter 并指定拦截规则。addListener(Class<? extends EventListener> listenerClass):动态注册监听器。
需要注意,动态注册方法仅允许在应用初始化期间(即 ServletContextListener.contextInitialized 执行期间或 ServletContainerInitializer.onStartup 中)调用,应用初始化完成一旦对外提供服务,该操作将被容器严格锁定。
容器底层实现
在 Servlet 容器底层(以 Apache Tomcat 为例),ServletContext 的实现采用了标准的外观模式(Facade Pattern)。

如上图所示,在 Tomcat 的层级架构中:
Context(实现类为StandardContext):对应图中的 Context 层级,表示一个运行中的 Web 应用,内部管理着多个 Wrapper(即各个具体的 Servlet)。ApplicationContext:StandardContext内部维护的核心上下文对象,承载容器大部分具体逻辑。ApplicationContextFacade:为了防止开发者编写的业务代码直接将接口强制转型为StandardContext并调用底层敏感的管理方法(如卸载应用、停止服务),Tomcat 向用户暴露的是ApplicationContextFacade外观类,所有对外调用的 API 最终透传给内部的ApplicationContext,起到了严格的安全防护与解耦作用。
四大域对象对比
Servlet 规范中定义了四大作用域对象,其生存周期与可见范围逐级递增:
| 域对象 | 对应接口 | 生命周期 | 共享范围 |
|---|---|---|---|
| Page Scope | PageContext | 当前 JSP 页面解析执行期间 | 单个页面内部 |
| Request Scope | HttpServletRequest | 一次 HTTP 请求从到达至响应完毕 | 同一请求链条(含请求转发) |
| Session Scope | HttpSession | 客户端会话建立至超时失效或注销 | 同一客户端多次请求之间 |
| Application Scope | ServletContext | Web 应用加载启动至容器停止卸载 | 整个 Web 应用的所有用户与组件 |
API: ServletContext
上下文元数据与全局配置
StringgetContextPath():(),获取应用上下文路径。返回当前 Web 应用程序挂载的基础 URI 路径(以/开头,根上下文返回空字符串"")。StringgetInitParameter():(String name),读取全局初始化参数。根据参数键名获取整个 Web 应用上下文共享的静态配置字符串。Enumeration<String>getInitParameterNames():(),获取所有全局参数名。返回当前应用中配置的所有全局初始化参数名称的枚举集合。booleansetInitParameter():(String name, String value),动态写入全局参数。向应用上下文添加全局配置项。仅允许在上下文初始化阶段调用。intgetMajorVersion():(),获取 Servlet 规范主版本号。返回当前 Web 容器支持的 Servlet 规范主版本(例如 Servlet 3.1 返回 3)。intgetMinorVersion():(),获取 Servlet 规范次版本号。返回当前 Web 容器支持的 Servlet 规范次版本(例如 Servlet 3.1 返回 1)。StringgetServerInfo():(),获取容器环境标识。返回当前 Servlet 容器的软件名称和版本信息(例如Apache Tomcat/9.0.65)。StringgetServletContextName():(),获取显示名称。返回在web.xml的<display-name>标签中定义的应用程序描述名称。
注意事项:
setInitParameter时机锁定:setInitParameter(name, value)仅能在ServletContextListener.contextInitialized执行阶段或 Servlet 容器初始化入口中调用。一旦整个应用完成初始化并对外暴露 HTTP 服务,该方法将直接返回false且不生效。getContextPath结尾斜杠:该方法返回的路径绝不以/结尾(除非当前应用本身即为根上下文/,此时返回空字符串"")。在进行 URI 拼接时,开发者必须自行补齐前导斜杠(例如context.getContextPath() + "/api/v1")。
package com.example.servlet.context.api;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ContextMetadataApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
ServletContext context = getServletContext();
// 1. 读取上下文挂载与容器环境元数据
String contextPath = context.getContextPath();
String serverInfo = context.getServerInfo();
int major = context.getMajorVersion();
int minor = context.getMinorVersion();
String appName = context.getServletContextName();
// 2. 尝试向运行时写入全局参数(此时已过初始化期,应返回 false)
boolean writeSuccess = context.setInitParameter("runtimeKey", "runtimeValue");
resp.setContentType("text/plain;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("上下文路径: " + contextPath);
out.println("容器版本: " + serverInfo);
out.println("Servlet 规范版本: " + major + "." + minor);
out.println("应用注册名: " + appName);
out.println("运行时写入参数结果: " + writeSuccess);
// 3. 遍历全局配置参数
out.println("--- 全局参数列表 ---");
Enumeration<String> params = context.getInitParameterNames();
while (params.hasMoreElements()) {
String paramKey = params.nextElement();
out.println(paramKey + " = " + context.getInitParameter(paramKey));
}
}
}全局属性共享与状态管理
ObjectgetAttribute():(String name),读取全局共享属性。根据指定键名获取存储在 Application 作用域下的对象引用。若属性不存在则返回null。Enumeration<String>getAttributeNames():(),获取所有属性名称。返回当前上下文中绑定的所有全局属性键名集合。voidsetAttribute():(String name, Object object),设置全局共享属性。以键值对形式绑定对象到 Application 作用域。若传入的对象为null,等效于调用removeAttribute。voidremoveAttribute():(String name),移除全局共享属性。从 Application 作用域中根据键名彻底清除绑定的对象。
注意事项:
- 内存泄漏规避:存入
ServletContext的对象其生命周期与 JVM 中的 Web 应用等长。严禁在全局属性中存放生命周期短暂的大对象(如超大请求体 ByteArray、未关闭的连接池引用),否则这些对象将长期驻留老年代,导致持续 Full GC 甚至引发OutOfMemoryError。- 监听器事件级联触发:每次调用
setAttribute、removeAttribute时,容器会同步阻塞触发已注册的ServletContextAttributeListener监听器。若监听器回调中存在重度计算或网络 IO,将直接拖慢调用方的工作线程响应速度。
package com.example.servlet.context.api;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ContextAttributeApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
ServletContext context = getServletContext();
// 1. 存入全局属性
context.setAttribute("SERVICE_STATUS", "ONLINE");
context.setAttribute("NODE_ID", "NODE-SH-01");
// 2. 读取全局属性
String serviceStatus = (String) context.getAttribute("SERVICE_STATUS");
// 3. 遍历所有全局属性键名
Enumeration<String> attributeNames = context.getAttributeNames();
StringBuilder namesBuilder = new StringBuilder();
while (attributeNames.hasMoreElements()) {
namesBuilder.append(attributeNames.nextElement()).append(", ");
}
// 4. 清理指定属性
context.removeAttribute("NODE_ID");
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().write(String.format("服务状态: %s\n全量属性键: %s\nNODE_ID 移除后校验: %s",
serviceStatus, namesBuilder.toString(), context.getAttribute("NODE_ID")));
}
}物理路径解析与资源读取
StringgetRealPath():(String path),虚拟路径转操作系统物理路径。将基于 Web 根目录的虚拟文件系统路径解析为绝对文件系统路径。InputStreamgetResourceAsStream():(String path),以输入流读取静态资源。以流的方式直接读取指定相对路径的应用内部资源。URLgetResource():(String path),获取资源的统一资源定位符。返回指向目标资源的java.net.URL对象,用于进一步提取 URL 连接元数据。StringgetMimeType():(String file),解析文件的 MIME 类型。根据文件名后缀匹配容器配置的媒体传输格式(例如输入avatar.png返回image/png)。
注意事项:
- 前导斜杠硬性要求:调用
getRealPath(path)、getResourceAsStream(path)与getResource(path)时,传入的路径参数必须严格以/开头,代表当前 Web 应用程序的根路径(WebRoot)。若省略开头的斜杠,容器将抛出MalformedURLException或静默返回null。- 安全隔离目录访问:
/WEB-INF/与/META-INF/目录虽然受到容器保护禁止客户端通过浏览器直接发起 HTTP 访问,但通过ServletContext.getResourceAsStream("/WEB-INF/...")是完全被允许且安全的,是存放私有系统配置与敏感依赖的标准位置。
package com.example.servlet.context.api;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ResourceResolutionApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
ServletContext context = getServletContext();
// 1. 物理路径推导
String realPath = context.getRealPath("/index.html");
// 2. URL 对象解析
URL resourceUrl = context.getResource("/WEB-INF/web.xml");
// 3. 安全的数据流获取
InputStream stream = context.getResourceAsStream("/WEB-INF/web.xml");
boolean streamAvailable = (stream != null);
if (stream != null) {
stream.close();
}
// 4. MIME 媒体类型推断
String htmlMime = context.getMimeType("sample.html");
String jsonMime = context.getMimeType("response.json");
String pdfMime = context.getMimeType("document.pdf");
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().write(String.format(
"物理路径: %s\n资源 URL: %s\n流可用性: %b\nHTML MIME: %s\nJSON MIME: %s\nPDF MIME: %s",
realPath, resourceUrl, streamAvailable, htmlMime, jsonMime, pdfMime
));
}
}请求转发与系统日志
RequestDispatchergetRequestDispatcher():(String path),路径请求转发器。根据指定的相对或绝对路径创建转发调度器。RequestDispatchergetNamedDispatcher():(String name),命名组件请求转发器。根据部署时声明的 Servlet 逻辑别名定位并创建转发调度器。voidlog():(String msg),写入容器日志。向 Web 容器专有的应用程序日志文件中输出普通文本信息。voidlog():(String message, Throwable throwable),写入异常日志。向 Web 容器日志输出描述信息以及附带的异常堆栈跟踪。
注意事项:
- 路径相对性与 ServletRequest 的差异:
ServletContext.getRequestDispatcher(path)中的路径必须以/开头,从当前 Web 应用程序的根路径起算;而ServletRequest.getRequestDispatcher(path)允许使用不带斜杠的相对路径(相对于当前 Servlet 的 URL 映射)。- 日志重定向问题:
context.log(...)的底层实现依赖宿主容器自身的日志配置(如 Tomcat 的catalina.out或localhost.log),它不支持现代日志框架(SLF4J、Logback)的高级路由、日志切割与异步队列,仅推荐用于容器生命周期底层的故障诊断排查。
package com.example.servlet.context.api;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class DispatchAndLoggingApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
ServletContext context = getServletContext();
// 1. 通过系统日志打印诊断信息
context.log("[DispatchDemo] 开始执行系统分发链路调度");
try {
// 2. 基于绝对虚拟路径获取转发器
RequestDispatcher pathDispatcher = context.getRequestDispatcher("/target-path");
// 3. 基于 Servlet 命名别名获取转发器
RequestDispatcher namedDispatcher = context.getNamedDispatcher("TargetComponentServlet");
// 4. 模拟条件转发
if (req.getParameter("useNamed") != null && namedDispatcher != null) {
namedDispatcher.forward(req, resp);
} else if (pathDispatcher != null) {
pathDispatcher.forward(req, resp);
}
} catch (Exception ex) {
// 5. 记录带异常堆栈的错误日志
context.log("[DispatchDemo] 分发调用链异常断裂", ex);
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "分发路由失败");
}
}
}动态组件注册
ServletRegistration.DynamicaddServlet():(String servletName, Class<? extends Servlet> servletClass),动态注册 Servlet。在运行期通过类类型向容器中注册全新的 Servlet 组件并返回可配置实体。FilterRegistration.DynamicaddFilter():(String filterName, Class<? extends Filter> filterClass),动态注册 Filter。在运行期通过类类型向应用上下文中注册过滤器组件。voidaddListener():(Class<? extends EventListener> listenerClass),动态注册监听器。在运行期向应用注册事件监听器(如生命周期监听器、会话监听器)。SessionCookieConfiggetSessionCookieConfig():(),获取会话 Cookie 配置对象。返回用于自定义管理JSESSIONIDCookie 属性(如HttpOnly、Secure、Path、Domain)的配置门面。
注意事项:
- 注册窗口期锁闭:所有
addServlet、addFilter和addListener动态配置接口,仅被允许在应用启动阶段(即ServletContextListener.contextInitialized执行期间或ServletContainerInitializer.onStartup方法中)被调用。一旦容器完全就绪开始接收请求,在doGet、doPost等服务方法中调用动态注册接口将直接抛出IllegalStateException。- 类加载器一致性:使用
Class或全类名注册组件时,必须确保传入的类型能被当前 Web 应用专有的类加载器(如 Tomcat 的WebappClassLoader)正确加载,避免因双亲委派机制冲突导致类转换失败。
package com.example.servlet.context.api;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletRegistration;
import javax.servlet.SessionCookieConfig;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServlet;
@WebListener
public class DynamicRegistrationApiDemo implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
// 1. 动态挂载 Servlet 组件
ServletRegistration.Dynamic dynamicServlet =
context.addServlet("DynamicApiServlet", HttpServlet.class);
if (dynamicServlet != null) {
dynamicServlet.addMapping("/dynamic-api/*");
dynamicServlet.setLoadOnStartup(2);
dynamicServlet.setAsyncSupported(true);
}
// 2. 动态挂载 Filter 过滤器
FilterRegistration.Dynamic dynamicFilter =
context.addFilter("DynamicSecurityFilter", Filter.class);
if (dynamicFilter != null) {
dynamicFilter.addMappingForUrlPatterns(
EnumSet.of(DispatcherType.REQUEST), true, "/*");
}
// 3. 动态配置全局会话 Cookie 增强安全性
SessionCookieConfig cookieConfig = context.getSessionCookieConfig();
cookieConfig.setHttpOnly(true);
cookieConfig.setSecure(false);
cookieConfig.setName("SECURE_JSESSIONID");
// 4. 动态添加自定义事件监听器
context.addListener(DynamicRegistrationApiDemo.class);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 上下文销毁钩子
}
}实战:网站计数器
设计原理
在韩顺平老师的 JavaWeb 教学体系中,ServletContext 常被形象地比作“教室前方的大黑板”:
- 黑板效应:只要 Web 应用程序不重启,这块黑板就始终存在;任何一位同学(客户端浏览器)走进来,都能看到黑板上的内容,也可以在上面修改数字。
- 作用域差异:
HttpServletRequest相当于单次传递的纸条,HttpSession相当于每个学生独立的储物柜,而ServletContext则是全校师生公共共享的全局空间。 - 核心机制:利用该对象在整个应用生命周期中全局唯一且所有用户共享的特性,将访问次数作为属性存储在其中。

代码实现
仅使用单个 Servlet 类实现访问计数,通过重写 doGet 方法完成数据的读取、累加与回写:
package com.hspedu.servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(urlPatterns = "/visit")
public class VisitCountServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 设置响应格式与编码,防止输出中文乱码
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
// 2. 获取当前 Web 应用全局唯一的 ServletContext 对象
ServletContext servletContext = getServletContext();
// 3. 从 ServletContext 中读取名为 visitCount 的全局属性
Object visitCount = servletContext.getAttribute("visitCount");
// 4. 判断是否首次访问:若无记录则初始化为1,已有记录则递增
if (visitCount == null) {
servletContext.setAttribute("visitCount", 1);
visitCount = 1;
} else {
int count = Integer.parseInt(visitCount.toString()) + 1;
servletContext.setAttribute("visitCount", count);
visitCount = count;
}
// 5. 将当前统计值响应回浏览器客户端
out.println("<h1>网站访问统计</h1>");
out.println("<p>当前网站累计访问量为: " + visitCount + "</p>");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}执行流程
用户在浏览器地址栏输入
http://localhost:8080/项目名/visit发起请求。Tomcat 容器接收请求,匹配并调用
VisitCountServlet的doGet方法。执行
getServletContext()方法,取得当前 Web 应用的全局上下文。调用
getAttribute("visitCount")检查上下文域中是否已存储计数器。若返回值为
null,说明是 Web 应用启动后的首次访问,设置计数值为1并写入上下文。若返回值不为
null,将取出的计数值转换为整型并加1,随后调用setAttribute覆盖旧值。组装 HTML 文本,通过字符输出流将最新的统计结果输出至浏览器展示。
线程安全
在多用户高并发同时访问时,ServletContext 中的属性读写可能出现并发竞争问题(即读取与写回非原子操作导致计数丢失)。
可以通过将 servletContext 作为锁对象进行同步保护:
ServletContext servletContext = getServletContext();
Object visitCount;
// 使用当前唯一的全局上下文对象加锁保证并发安全
synchronized (servletContext) {
visitCount = servletContext.getAttribute("visitCount");
if (visitCount == null) {
servletContext.setAttribute("visitCount", 1);
visitCount = 1;
} else {
int count = Integer.parseInt(visitCount.toString()) + 1;
servletContext.setAttribute("visitCount", count);
visitCount = count;
}
}- 加锁对象选择:必须锁定唯一的全局对象(如
servletContext),不可使用this,因为不同请求如果被不同实例处理则无法形成互斥。
HttpServletRequest
核心定位
是 Java Web 开发中最基础的核心接口之一。当客户端浏览器向 Servlet 容器(如 Apache Tomcat)发送一条 HTTP 请求时,容器负责解析底层的 TCP 字节流,并将其完整封装为一个实现了 javax.servlet.http.HttpServletRequestHttpServletRequest 接口的对象。
- 数据载体:封装了客户端发来的完整 HTTP 请求报文,包含请求行、请求头、空行和请求体。
- 通信接口:作为开发者与客户端请求交互的标准入口,提供统一的 API 读取参数、识别客户端环境、管理单次请求生命周期内的数据流转。
报文映射
HTTP 协议基于纯文本传输,HttpServletRequest 的各类 API 与底层的 HTTP 报文结构有着严格的映射关系。
- 请求行:包含请求方法、请求路径(URL)和 HTTP 协议版本,对应
getMethod()、getRequestURI()等方法。 - 首部行(请求头):包含键值对形式的元数据(如
Host、User-Agent、Cookie),对应getHeader()等方法。 - 空行:作为协议标准的分隔符,用于标记首部结束与请求体开始。
- 实体主体(请求体):承载 POST、PUT 等请求携带的表单数据或 JSON 负载,对应
getParameter()、getInputStream()等方法。

继承体系
HttpServletRequest 并不是孤立存在的接口,它处于 Servlet 规范的继承与实现体系中:
ServletRequest:顶层通用接口,定义了与具体应用层协议无关的基础方法(如获取参数、字符编码、域属性操作)。HttpServletRequest:继承自ServletRequest,专门针对 HTTP/HTTPS 协议进行扩展,补充了请求行、请求头、Cookie、Session 及请求转发等 HTTP 专用 API。- 容器实现类:在 Tomcat 内部,实际运行的对象为
org.apache.catalina.connector.RequestFacade(外观模式实现),它包装了底层的Request实体,防止业务代码触碰容器内部的管理方法。

生命周期
HttpServletRequest 是典型的请求级别对象,生命周期短暂且与单次 HTTP 请求强绑定:
建立连接与解析:客户端发起 HTTP 请求,Tomcat 连接器(Connector)捕获连接,读取网络流并解析 HTTP 协议格式。
实例化对象:容器创建
HttpServletRequest与HttpServletResponse实例。路由分发:容器根据 URL 映射规则定位到目标 Servlet,调用其
service()方法,并将 request 与 response 对象作为参数传入。业务处理:Servlet 读取 request 中的数据并执行业务逻辑(如校验、查询、转发)。
响应完成与销毁:服务端响应返回给客户端后,该次 HTTP 请求结束,容器立刻将 request 对象标记为失效并销毁(或回收至对象池),内存由 GC 清理。
请求转发
请求转发(Request Forwarding) 是 Servlet 规范提供的一种服务器端内部资源调度机制。
当客户端向服务器发送请求后,当前 Servlet 并不直接生成最终响应,而是通过服务器内部的调度器,将请求对象(HttpServletRequest)和响应对象(HttpServletResponse)原封不动地传递给另一个资源(可以是另一个 Servlet、JSP 或静态 HTML 页面),由目标资源继续处理并负责向浏览器输出结果。
整个流转过程完全在服务器内部封闭进行,客户端浏览器对此毫无感知。
运作机制
请求转发最显著的特征是单次 HTTP 请求闭环。客户端只与服务端建立一次网络连接,浏览器地址栏保持初次请求的 URL 不变。

如上图所示,整体执行包含四个核心阶段:
客户端发起请求:用户浏览器向
Servlet1发起 HTTP 请求。服务器内部转发:
Servlet1调用forward()方法,把请求和响应交由Servlet2承接。目标资源处理:
Servlet2读取请求上下文并生成最终的响应实体。统一回写响应:由 Web 容器将响应体直接返回给客户端浏览器。
执行流程:

获取调度器方式
- 通过
request.getRequestDispatcher(String path)获取,路径支持相对路径或以/开头的绝对路径。 - 通过
servletContext.getRequestDispatcher(String path)获取,路径必须以/开头(相对于当前 Web 应用根目录)。
API: RequestDispatcher
voidforward():(ServletRequest request, ServletResponse response),服务端请求转发。将请求从当前 Servlet 移交给同一应用中的另一资源(Servlet、JSP 或静态资源),交出最终响应控制权。voidinclude():(ServletRequest request, ServletResponse response),服务端内容包含。将目标资源的响应输出内容嵌套、并入到当前 Servlet 的输出流中,保留自身控制权。
注意事项:
- 响应提交后转发异常:必须在调用
forward()之前使用response.isCommitted()进行安全前置探测。一旦底层缓冲区已向网络连接发送数据,触发forward()将抛出IllegalStateException。- 转发后代码穿透陷阱:
forward()与include()是同步方法调用,并不是return语句。当目标资源执行完毕后,执行流程仍会返回到forward()调用的下一行代码继续向下运行。如果在forward()后没有紧随return;,后续代码修改变量或抛出异常将产生不可预期的系统副作用。- 死循环转发规避:严禁在资源链中构建无终结条件的循环转发(例如组件 A
forward至组件 B,组件 B 又forward至组件 A),这会导致容器的工作线程栈空间迅速耗尽,直接抛出StackOverflowError。
package com.example.servlet.dispatcher.api;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CoreDispatcherApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String action = req.getParameter("action");
if ("includeDemo".equals(action)) {
RequestDispatcher dispatcher = req.getRequestDispatcher("/fragments/nav-bar.html");
if (dispatcher != null) {
// 1. 将外部静态模块片段包含到当前响应中
dispatcher.include(req, resp);
}
return;
}
RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/dashboard.jsp");
if (dispatcher != null) {
// 2. 防御性检查是否已经提交响应
if (!resp.isCommitted()) {
// 执行请求转发并彻底交出响应流控制权
dispatcher.forward(req, resp);
// 必须紧跟 return,防止后续代码发生穿透执行
return;
}
}
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "目标分发视图不可用");
}
}代码实现
编写两个 Servlet,演示由 SourceServlet 处理业务并向 TargetServlet 转发请求:
package com.example.servlet;
import javax.servlet.RequestDispatcher;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/source")
public class SourceServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1. 向当前请求域存放业务共享数据
req.setAttribute("role", "admin");
// 2. 获取请求调度器并指定目标资源路径
RequestDispatcher dispatcher = req.getRequestDispatcher("/target");
// 3. 执行转发将当前请求与响应流转至目标组件
dispatcher.forward(req, resp);
}
}目标组件接收请求并提取数据:
// 从请求域提取由前置 Servlet 存放的共享数据
Object role = req.getAttribute("role");
// 输出响应内容交付客户端
resp.getWriter().println("Target received role: " + role);数据共享
请求转发之所以能够在多个组件间无缝传递数据,核心原因在于作用域对象的共享:
- 对象同一性:在整条转发链路中,从起点组件到终点组件,操作的都是容器初始创建的同一个
HttpServletRequest内存实例。 - 数据流转通道:通过
req.setAttribute("key", value)设值,目标组件通过req.getAttribute("key")取值,只要单次请求未结束,数据便始终有效。 - 自动垃圾回收:当目标资源渲染完毕并最终响应给客户端后,该次 HTTP 请求彻底结束,
request对象被容器回收,域中存储的临时数据随之销毁,不会造成全局内存泄露。
路径规范
在编写转发路径时,路径格式与访问权限具有严格的规则:
- 斜杠开头(绝对路径):形如
req.getRequestDispatcher("/user/list"),此处的斜杠/代表当前 Web 应用程序的根路径(即http://localhost:8080/工程名/),无需也不可手动拼接项目名(Context Path)。 - 非斜杠开头(相对路径):形如
req.getRequestDispatcher("detail"),相对于当前发起转发的 Servlet 所在的 URI 路径进行相对寻址。 - 越权保护能力:Web 应用的
/WEB-INF/目录属于受保护资源,浏览器直接在地址栏输入 URL 访问会被服务器直接拦截并返回 404;但通过服务端的forward()方法可以合法且安全地访问/WEB-INF/内部的 JSP 或 HTML 文件,常用于防止用户绕过认证直接访问受保护页面。 - 局限性:请求转发完全依托于当前 Web 容器环境,因此无法转发到外部站点(如无法转发到
[http://www.baidu.com](http://www.baidu.com))。 - 重复支付问题:因为浏览器地址栏会停止在第一个servlet ,如果你刷新页面, 会再次发出请求(并且会带数据),所以在支付页面情况下, 不要使用请求转发, 否则会造成重复支付。
转发与重定向
请求转发(Forward)与客户端重定向(Redirect)是 Web 开发中极易混淆的两种页面跳转方式:
| 比较维度 | 请求转发(Forward) | 客户端重定向(Redirect) |
|---|---|---|
| 跳转位置 | 服务器端内部调度流转 | 客户端浏览器重新发起连接 |
| HTTP 请求次数 | 仅 1 次网络请求 | 至少 2 次网络请求 |
| 浏览器地址栏 | 保持初始请求 URL 不变 | 变为最终重定向的新 URL |
| 数据共享性 | 共享同一个 request 域数据 | 产生新请求,原 request 域失效 |
| 目标资源范围 | 仅限当前 Web 应用内部资源 | 可以是当前项目或任意外部 URL |
| 受保护目录 | 能够直接访问 /WEB-INF/ 资源 | 无法直接访问 /WEB-INF/ 资源 |
| 核心 API | request.getRequestDispatcher().forward() | response.sendRedirect() |
乱码处理
由于客户端编码与服务端解码采用的字符集可能存在差异,需要针对性地处理中文乱码:
// 针对 POST 请求体中的字节流指定服务端统一解码编码
req.setCharacterEncoding("UTF-8");
// 仅需在首次调用读取参数前设定一次即可生效
String username = req.getParameter("username");- POST 请求乱码:在读取任何参数(如调用
getParameter())之前,必须先调用req.setCharacterEncoding("UTF-8")。 - GET 请求乱码:GET 参数位于请求行 URL 中,编码由容器的连接器决定。Tomcat 8.0 及以上版本默认使用 UTF-8 对 URI 进行解码,通常无需手动配置;旧版本需在
server.xml的<Connector>标签中显式配置URIEncoding="UTF-8"。
API: HttpServletRequest
HttpServletRequest 是纯接口,其实例由容器运行时注入;对于装饰器扩展场景,标准库提供了 HttpServletRequestWrapper 包装类构造方法。
构造方法
HttpServletRequestWrapperHttpServletRequestWrapper():(HttpServletRequest request),包装器构造方法。基于装饰器模式构建一个适配包装对象,用于安全拦截并重写请求处理逻辑(如重写输入流、重定向参数等)。
注意事项:
- 传入的被包装对象
request不得为null,否则抛出IllegalArgumentException。HttpServletRequestWrapper默认实现将所有方法调用原封不动地透传给底层被包装的实例,开发者仅需针对性重写需要变更逻辑的目标方法。
package com.example.servlet.request.api;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class ConstructorApiDemo {
public void demonstrateWrapperCreation(HttpServletRequest sourceRequest) {
// 创建通用的 HTTP 请求包装器实例
HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(sourceRequest) {
@Override
public String getHeader(String name) {
if ("X-Custom-Auth".equalsIgnoreCase(name)) {
return "Injected-Token-Value";
}
return super.getHeader(name);
}
};
}
}请求行
StringgetMethod():(),获取 HTTP 请求动词。返回客户端请求所使用的标准 HTTP 动词名称(如GET、POST、PUT、DELETE)。StringgetRequestURI():(),获取请求统一资源标识符。返回从协议主机名后开始到查询字符串(QueryString)之前的相对路径(包含 ContextPath)。StringBuffergetRequestURL():(),获取请求完整资源定位符。返回包含协议、主机名、端口号与 URI 路径的完整 URL 构建器,不包含查询字符串。StringgetContextPath():(),获取应用上下文挂载路径。返回当前 Web 应用程序挂载的基础路径(根上下文返回空字符串"")。StringgetServletPath():(),获取当前 Servlet 映射路径。返回触发当前 Servlet 调用的匹配 URL 规则路径部分。StringgetPathInfo():(),获取额外路径信息。返回在 ServletPath 之后、QueryString 之前的附加路径。若无通配匹配则返回null。StringgetQueryString():(),获取原始查询字符串。返回 URL 中?之后的原始查询串,未经过 URL 解码。若无参数则返回null。StringgetProtocol():(),获取协议版本。返回请求使用的网络协议名称及版本(如HTTP/1.1或HTTP/2.0)。
注意事项:
getRequestURL()返回类型的非易失性:该方法返回的是StringBuffer而非String,主要是 Servlet 早期规范的历史遗留设计。由于其未实现不可变性,严禁将其直接共享给多线程修改。getRequestURI()防范路径穿越:getRequestURI()返回的是未经规范化的原始路径,如果请求中包含..、//等特殊符号,在进行权限认证或安全路径比对前,必须使用安全工具(如 Spring 的UrlPathHelper或 Java 原生Path.normalize())进行路径归一化,防止路径遍历越权漏洞。
package com.example.servlet.request.api;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class RequestLineApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 提取 HTTP 请求动词与传输协议
String method = req.getMethod();
String protocol = req.getProtocol();
// 2. 提取路径层次与定位符
String uri = req.getRequestURI();
StringBuffer urlBuffer = req.getRequestURL();
String contextPath = req.getContextPath();
String servletPath = req.getServletPath();
String pathInfo = req.getPathInfo();
String queryString = req.getQueryString();
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().write(String.format(
"Method: %s\nProtocol: %s\nURI: %s\nURL: %s\nContext: %s\nServlet: %s\nPathInfo: %s\nQuery: %s",
method, protocol, uri, urlBuffer.toString(), contextPath, servletPath, pathInfo, queryString
));
}
}请求头
StringgetHeader():(String name),读取指定单值请求头。返回指定 Header 名称的字符串取值。若 Header 不存在则返回null。Enumeration<String>getHeaders():(String name),读取同名多值请求头。返回指定 Header 名称所关联的所有值的枚举集合(例如包含多个Accept或Cookie)。Enumeration<String>getHeaderNames():(),获取所有请求头名称。返回当前请求报文中携带的所有 Header 键名的枚举集合。intgetIntHeader():(String name),读取整型请求头。将指定请求头的值直接转换为int。若不存在返回-1,格式不正确抛出NumberFormatException。longgetDateHeader():(String name),读取日期型请求头。将包含 HTTP 日期格式(如 RFC 1123)的请求头自动转换为自 Epoch 起算的毫秒长整型时间戳。StringgetContentType():(),获取请求体 MIME 媒体类型。返回 Header 中Content-Type的完整取值(如application/json;charset=UTF-8)。intgetContentLength():(),获取请求体字节长度。返回 Header 中Content-Length的整型取值。若长度未知则返回-1。
注意事项:
- 大小写不敏感规范:按照 RFC 7230 规范,HTTP Header 的名称是不区分大小写的。因此调用
getHeader("Accept")与getHeader("accept")获取的结果完全相同。- 超大请求体长度截断:
getContentLength()返回值最大为Integer.MAX_VALUE(约 2GB)。若处理大文件上传或超大流,应改用 Servlet 3.1 引入的getContentLengthLong()方法获取long型长度,防止发生数值整型溢出。
package com.example.servlet.request.api;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class RequestHeadersApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain;charset=UTF-8");
PrintWriter out = resp.getWriter();
// 1. 读取单值与类型特化请求头
String userAgent = req.getHeader("User-Agent");
int contentLength = req.getContentLength();
long ifModifiedSince = req.getDateHeader("If-Modified-Since");
out.println("User-Agent: " + userAgent);
out.println("Content-Length: " + contentLength);
out.println("If-Modified-Since 时间戳: " + ifModifiedSince);
// 2. 遍历所有请求头
out.println("\n--- 全量请求头遍历 ---");
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
// 读取同名多值请求头
Enumeration<String> values = req.getHeaders(name);
StringBuilder valBuilder = new StringBuilder();
while (values.hasMoreElements()) {
valBuilder.append(values.nextElement()).append("; ");
}
out.println(name + " -> " + valBuilder.toString());
}
}
}请求参数
StringgetParameter():(String name),获取单值参数。根据参数名获取单个字符串参数值。若存在多个同名参数,仅返回容器解析出的第一个值。String[]getParameterValues():(String name),获取多值参数数组。根据参数名获取同名参数的所有取值组成的数组(常见于多选 Checkbox 提交)。Map<String, String[]>getParameterMap():(),获取不可变参数映射表。返回包含当前请求所有参数键值对的 Map 集合。Enumeration<String>getParameterNames():(),获取所有参数名称。返回当前请求中所有参数键名的枚举集合。voidsetCharacterEncoding():(String env),设置请求体解码字符集。显式覆盖容器解析 Body 参数时所使用的字符编码集(如UTF-8)。StringgetCharacterEncoding():(),获取当前字符编码。返回在请求头Content-Type中指定的字符集,若未明确声明且未设置过则返回null。
注意事项:
getParameterMap()的不可变性:容器返回的Map<String, String[]>是受保护的只读视图(如 Tomcat 的ParameterMap.setLocked(true))。直接对其执行put或remove会抛出IllegalStateException或UnsupportedOperationException。- GET 与 POST 参数的聚合性:
getParameter系列方法是聚合性的,它会同时合并 URL QueryString 与 POST Form-urlencoded Body 中的参数。若两者包含同名键,QueryString 往往优先排列在String[]数组的前列。
package com.example.servlet.request.api;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class RequestParamsApiDemo extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 设置字符编码
req.setCharacterEncoding("UTF-8");
String encoding = req.getCharacterEncoding();
resp.setContentType("text/plain;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("当前解析使用的字符编码: " + encoding);
// 2. 读取单值与多值参数
String singleParam = req.getParameter("username");
String[] multiParams = req.getParameterValues("hobbies");
out.println("单值参数 username: " + singleParam);
out.println("多值参数 hobbies: " + Arrays.toString(multiParams));
// 3. 遍历参数映射表 Map
out.println("\n--- 参数 Map 结构 ---");
Map<String, String[]> paramMap = req.getParameterMap();
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
out.println(entry.getKey() + " -> " + Arrays.toString(entry.getValue()));
}
// 4. 遍历所有参数键名
out.println("\n--- 参数名枚举遍历 ---");
Enumeration<String> names = req.getParameterNames();
while (names.hasMoreElements()) {
out.println("检测到参数名: " + names.nextElement());
}
}
}请求体
ServletInputStreamgetInputStream():(),获取二进制输入流。返回用于读取请求体原始二进制字节数据的输入流通道。BufferedReadergetReader():(),获取字符输入缓冲读取器。返回经过字符集解码包装的缓冲字符流,常用于读取 JSON、XML 格式的请求体。
注意事项:
- 互斥抛出规则:对于同一个请求对象,调用了
getInputStream()之后,再调用getReader()会直接抛出IllegalStateException;反之亦然。- 异步非阻塞 IO 升级:在 Servlet 3.1+ 规范中,
ServletInputStream新增了setReadListener(ReadListener listener)方法,支持结合@WebServlet(asyncSupported = true)实现基于事件驱动的非阻塞流读取,适用于高并发网关代理。
package com.example.servlet.request.api;
import java.io.BufferedReader;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class RequestBodyStreamApiDemo extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String contentType = req.getContentType();
resp.setContentType("text/plain;charset=UTF-8");
// 根据 Content-Type 策略选择字符流或字节流
if (contentType != null && contentType.contains("application/json")) {
// 1. 使用 getReader 读取结构化字符文本
StringBuilder jsonPayload = new StringBuilder();
try (BufferedReader reader = req.getReader()) {
String line;
while ((line = reader.readLine()) != null) {
jsonPayload.append(line);
}
}
resp.getWriter().write("成功以字符流消费 JSON: " + jsonPayload.toString());
} else {
// 2. 使用 getInputStream 读取二进制原生数据
ServletInputStream inputStream = req.getInputStream();
byte[] binaryData = inputStream.readAllBytes();
resp.getWriter().write("成功以二进制字节流消费数据,长度: " + binaryData.length + " bytes");
}
}
}请求域与流程分发
ObjectgetAttribute():(String name),读取请求作用域属性。获取通过程序存入当前 Request 对象的对象引用。voidsetAttribute():(String name, Object o),存入请求作用域属性。绑定对象至 Request 域。若传入null,等效于调用removeAttribute。voidremoveAttribute():(String name),移除请求作用域属性。从 Request 域中清除指定的属性键。Enumeration<String>getAttributeNames():(),获取所有属性名称。返回当前 Request 对象绑定的所有属性键名枚举。RequestDispatchergetRequestDispatcher():(String path),获取请求分发调度器。用于将当前请求在服务端内部转发(Forward)至其他资源或包含(Include)外部页面。
注意事项:
- 生命周期的瞬时性:Request 属性的生命周期仅存在于单次客户端请求中。一旦响应向客户端提交结束,所有绑定的属性即告废弃。
- 服务端转发的透明性:通过
req.getRequestDispatcher(path).forward(req, resp)进行内部转发时,客户端浏览器的 URL 地址栏不会发生改变,且原先存入 Request 作用域的所有属性在目标 Servlet 中依然完全可用。
package com.example.servlet.request.api;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class RequestScopeAndDispatchApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 设置业务处理中间属性
req.setAttribute("OPERATOR_ID", "SYS_ADMIN_01");
req.setAttribute("TRACE_TOKEN", "UUID-10023-9981");
// 2. 检查与移除属性
req.removeAttribute("TRACE_TOKEN");
// 3. 读取属性并验证
Object operatorId = req.getAttribute("OPERATOR_ID");
// 4. 遍历所有属性名
Enumeration<String> attributeNames = req.getAttributeNames();
while (attributeNames.hasMoreElements()) {
String attrName = attributeNames.nextElement();
System.out.println("存在属性: " + attrName);
}
// 5. 将处理请求内部转发至内部渲染路径
RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/internal-view.jsp");
if (dispatcher != null && req.getParameter("forward") != null) {
dispatcher.forward(req, resp);
} else {
resp.getWriter().write("操作人: " + operatorId);
}
}
}会话
Cookie[]getCookies():(),获取请求携带的 Cookie 数组。从 HTTP 请求报文的Cookie标头中解析并返回客户端提交的所有 Cookie 实例数组。若客户端本次请求未携带任何 Cookie,则返回null。HttpSessiongetSession():(),获取当前会话对象(无则创建)。等效于getSession(true)。返回与当前请求关联的HttpSession对象;若当前请求尚未关联有效会话,则自动在服务端创建并分配一个新会话。HttpSessiongetSession():(boolean create),按需获取或创建会话对象。若当前请求存在有效会话则直接返回;若不存在有效会话:当create为true时新建并返回会话,当create为false时直接返回null(不会主动创建新会话)。
注意事项:
getCookies()空指针陷阱(NPE):当客户端首次访问、禁用 Cookie 或本次请求未包含任何 Cookie 时,req.getCookies()会直接返回null而非空数组。在遍历或对数组执行任何操作前,必须显式进行非空判定(cookies != null),否则极易引发NullPointerException。getSession()与getSession(false)的选用策略:
getSession()(等同于getSession(true))具有“副作用”:若会话不存在则强制创建新会话,并在响应中自动下发Set-Cookie: JSESSIONID=...。- 在身份验证校验、拦截器/过滤器鉴权、或无状态只读操作中,若只是为了判断用户是否已登录,必须优先使用
req.getSession(false)。若盲目调用getSession(),网络爬虫或高频未登录请求将导致服务器无节制地创建大量空 Session,迅速耗尽堆内存甚至引发 OOM。- Cookie 字符集与中文编解码:RFC 6265 规范限制 Cookie 值中不能包含分号、逗号、空格以及任意非 ASCII 字符。若需在 Cookie 中存储中文或复杂字符串,在通过
response.addCookie()写入前必须使用URLEncoder.encode(val, "UTF-8")编码,并在通过req.getCookies()读取后使用URLDecoder.decode(val, "UTF-8")进行解码。
package com.example.servlet.request.api;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class RequestSessionAndCookieApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain;charset=UTF-8");
PrintWriter out = resp.getWriter();
// 1. 读取并遍历客户端携带的 Cookie(防范 null 引发 NPE)
Cookie[] cookies = req.getCookies();
if (cookies != null) {
out.println("--- 客户端提交的 Cookie 列表 ---");
for (Cookie cookie : cookies) {
String name = cookie.getName();
// 对可能包含特殊字符或中文的内容进行 URL 解码
String value = URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8);
out.println(String.format("Cookie 键: %s, 解码值: %s", name, value));
}
} else {
out.println("客户端本次请求未携带任何 Cookie");
}
// 2. 按需读取已存在会话(只读操作,避免无意义创建 Session 消耗服务端内存)
HttpSession existingSession = req.getSession(false);
if (existingSession == null) {
out.println("\n当前无活跃会话(getSession(false) 返回 null)");
} else {
out.println("\n检测到已存在的活跃会话,Session ID: " + existingSession.getId());
}
// 3. 获取或强制新建会话,并存取会话域属性
HttpSession session = req.getSession(); // 等效于 req.getSession(true)
session.setAttribute("USER_AUTH", "AUTH_TOKEN_SAMPLE");
}
}网络信息
StringgetRemoteAddr():(),获取客户端 IP 地址。返回直接与当前服务器建立 TCP Socket 连接的客户端/反向代理 IP 字符串。intgetRemotePort():(),获取客户端源端口号。返回客户端建立 TCP 连接时所使用的临时端口号。StringgetLocalAddr():(),获取服务端接收 IP 地址。返回服务器本机承接该网络连接的网络接口 IP 地址。intgetServerPort():(),获取服务端服务端口。返回承接请求的目标虚拟主机端口号。booleanisSecure():(),是否为安全加密连接。指示当前请求是否通过 SSL/TLS(HTTPS)安全信道传输。
注意事项:
- 反向代理下的真实 IP 穿透:当部署架构中存在 Nginx、HAProxy、SLB 等反向代理或 CDN 时,
getRemoteAddr()获取到的将是最后一跳代理服务器的内网 IP,而非客户端真实 IP。获取真实 IP 必须优先依次提取X-Forwarded-For、X-Real-IP请求头并进行合法性校验。- 伪造 Header 欺骗防御:客户端可自行在请求头伪造
X-Forwarded-For。在安全性要求极高的场景下(如支付鉴权),必须校验反向代理链路的白名单,或在 Nginx 接入层统一强制重写覆盖该 Header。
package com.example.servlet.request.api;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class NetworkMetadataApiDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 网络底层 Socket 元数据
String socketClientIp = req.getRemoteAddr();
int socketClientPort = req.getRemotePort();
String serverLocalIp = req.getLocalAddr();
int serverPort = req.getServerPort();
boolean secureChannel = req.isSecure();
// 2. 穿透反向代理识别真实客户端 IP
String realClientIp = req.getHeader("X-Forwarded-For");
if (realClientIp == null || realClientIp.isEmpty() || "unknown".equalsIgnoreCase(realClientIp)) {
realClientIp = req.getHeader("X-Real-IP");
}
if (realClientIp == null || realClientIp.isEmpty() || "unknown".equalsIgnoreCase(realClientIp)) {
realClientIp = socketClientIp;
} else if (realClientIp.contains(",")) {
// 多级代理场景下,第一个 IP 为真实客户端 IP
realClientIp = realClientIp.split(",")[0].trim();
}
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().write(String.format(
"直连 Socket 客户端: %s:%d\n" +
"识别真实客户端 IP: %s\n" +
"服务端本地绑定: %s:%d\n" +
"通道是否加密 (HTTPS): %b\n",
socketClientIp, socketClientPort, realClientIp, serverLocalIp, serverPort, secureChannel
));
}
}实战:用户注册

需求分析与控件映射
本实战案例模拟典型的用户注册场景,综合运用 HttpServletRequest 核心参数获取 API。表单控件与后端解析方法的映射关系如下:
| 表单控件 | HTML 元素类型 | 对应参数名 (name) | 后端获取方法 | 核心说明 |
|---|---|---|---|---|
| 用户名称 | <input type="text"> | username | req.getParameter("username") | 单值文本,需预先配置请求体解码字符集 |
| 用户密码 | <input type="password"> | password | req.getParameter("password") | 单值密文输入 |
| 确认密码 | <input type="password"> | confirmPassword | req.getParameter("confirmPassword") | 服务端核验两次密码是否一致 |
| 运动项目 | <input type="checkbox"> | sports | req.getParameterValues("sports") | 多值参数,全未勾选时返回 null |
| 选择性别 | <input type="radio"> | gender | req.getParameter("gender") | 单选框,获取被选中的 value 取值 |
| 选择城市 | <select> 下拉菜单 | city | req.getParameter("city") | 单选下拉框,读取所选 <option> 的 value |
| 自我介绍 | <textarea> 多行文本 | intro | req.getParameter("intro") | 纯文本多行字符串,保留内部空白与换行 |
| 头像文件 | <input type="file"> | avatar | req.getPart("avatar") | 二进制多部件文件段,需搭配 @MultipartConfig |
前端表单设计(register.html)
前端表单采用 POST 方式提交至后端 Servlet,表单中包含文件选择框,需配置 enctype="multipart/form-data":
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>用户注册</title>
</head>
<body>
<h3>用户注册信息</h3>
<form action="register" method="post" enctype="multipart/form-data">
用户名称: <input type="text" name="username" /><br />
用户密码: <input type="password" name="password" /><br />
确认密码: <input type="password" name="confirmPassword" /><br />
选择你喜欢的运动项目:
<input type="checkbox" name="sports" value="篮球" />篮球<br />
<input type="checkbox" name="sports" value="足球" checked />足球<br />
<input type="checkbox" name="sports" value="手球" checked />手球<br />
请选择性别:
<input type="radio" name="gender" value="男" />男<br />
<input type="radio" name="gender" value="女" />女<br />
请选择城市:
<select name="city">
<option value="">--选择--</option>
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="广州">广州</option>
<option value="深圳">深圳</option></select
><br />
自我介绍:
<textarea name="intro" rows="5" cols="25"></textarea><br />
选择你的文件(头像) <input type="file" name="avatar" /><br />
<input type="submit" value="提交" />
<input type="reset" value="重置" />
</form>
</body>
</html>后端 Servlet 实现(RegisterServlet.java)
后端通过继承 HttpServlet 并重写 doPost 方法,演示单值、多值参数以及二进制文件段的提取与回显:
package com.hspedu.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
@WebServlet("/register")
@MultipartConfig // 启用 Servlet 3.0+ 原生文件上传解析支持
public class RegisterServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 设置请求体解码字符集(必须在首次读取参数前调用,彻底解决 POST 中文乱码)
req.setCharacterEncoding("UTF-8");
// 2. 设置响应头与输出流编码格式
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
// 3. 读取单值普通文本参数
String username = req.getParameter("username");
String password = req.getParameter("password");
String confirmPassword = req.getParameter("confirmPassword");
String gender = req.getParameter("gender");
String city = req.getParameter("city");
String intro = req.getParameter("intro");
// 4. 读取多值复选框参数(Checkbox 提交的数组)
String[] sports = req.getParameterValues("sports");
// 5. 提取二进制头像文件项
Part avatarPart = req.getPart("avatar");
String avatarFileName = (avatarPart != null && avatarPart.getSize() > 0)
? avatarPart.getSubmittedFileName()
: "未选择上传文件";
long avatarSize = (avatarPart != null) ? avatarPart.getSize() : 0;
// 6. 基础业务校验(核验密码一致性)
if (password == null || !password.equals(confirmPassword)) {
out.println("<h3 style='color:red;'>注册失败:两次输入的密码不一致!</h3>");
out.println("<p><a href='javascript:history.back()'>返回重新填写</a></p>");
return;
}
// 7. 回显展示接收并解析完毕的注册信息
out.println("<h2>用户注册成功!提交信息如下:</h2>");
out.println("<ul>");
out.println(" <li><strong>用户名称:</strong>" + username + "</li>");
out.println(" <li><strong>用户密码:</strong>" + password + "</li>");
out.println(" <li><strong>运动项目:</strong>" + (sports != null ? Arrays.toString(sports) : "未勾选任何项目") + "</li>");
out.println(" <li><strong>所选性别:</strong>" + (gender != null ? gender : "未选择") + "</li>");
out.println(" <li><strong>所属城市:</strong>" + (city != null && !city.isEmpty() ? city : "未选择") + "</li>");
out.println(" <li><strong>自我介绍:</strong><pre>" + intro + "</pre></li>");
out.println(" <li><strong>头像附件:</strong>" + avatarFileName + " (" + avatarSize + " 字节)</li>");
out.println("</ul>");
}
}核心细节与避坑指南
- Checkbox 多值参数的空指针防范:若用户未勾选任何运动选项直接点击提交,浏览器在请求体中不会携带
sports键,此时req.getParameterValues("sports")将返回null(而非空数组new String[0])。直接对sports进行遍历或访问sports.length会抛出NullPointerException,业务代码中务必执行sports != null的防御性校验。- POST 参数解码的时效性:
req.setCharacterEncoding("UTF-8")的生效前提是在首次调用任何getParameter()或getReader()之前执行。若在此之前读取过任何参数,底层流的解析解码器将被初始化并锁定,后续再次调用setCharacterEncoding()将完全失效,导致中文仍呈乱码状态。enctype="multipart/form-data"协同要求:当表单包含文件项时必须声明该编码类型;而在服务端,Servlet 必须显式标注@MultipartConfig注解。若遗漏该注解,Tomcat 不会解析多段数据,此时不仅req.getPart()会抛出异常,所有通过req.getParameter()提取的普通文本项也会全部变为null。
HttpServletResponse
是 Java Web 开发中用于封装 HTTP 响应信息的顶层接口。javax.servlet.http.HttpServletResponse
- 职责边界:当客户端向服务器发起请求时,Servlet 容器(如 Apache Tomcat)会同时创建一对对象——
HttpServletRequest与HttpServletResponse。HttpServletResponse专门负责向客户端浏览器发送 HTTP 状态码、响应头(Headers)以及实体内容(Body)。 - 协议绑定:该接口继承自通用的
javax.servlet.ServletResponse,并专门扩展了针对 HTTP/HTTPS 协议相关的方法(如重定向、Cookie 写入、状态码操控等)。
报文映射
HTTP 协议基于纯文本传输,HttpServletResponse 的所有核心方法均与 HTTP 响应报文结构保持一一对应。
- 状态行:包含协议版本、状态码与状态描述(如
HTTP/1.1 200 OK),对应setStatus()与sendError()。 - 响应头:键值对集合(如
Content-Type: text/html),对应setHeader()、addHeader()及各类快捷配置方法。 - 空行:回车换行符(CRLF),用于分隔首部与实体。
- 响应体(附属体):最终返回给浏览器渲染展示的数据,通过
getWriter()或getOutputStream()输出流逐字节/字符写入。

状态码控制
状态码由 3 位数字组成,用于向客户端指示该次请求的处理状态与结果类型:
// 设置自定义成功或操作状态码
resp.setStatus(200);
// 校验未通过时向客户端发送禁止访问状态码
resp.sendError(403, "Access Denied");voidsetStatus():(int sc),设置 HTTP 响应状态码。向响应状态行注入指定的标准 HTTP 状态码(如200、201、404等)。voidsendError():(int sc),派发标准错误状态码。向客户端发送指定的 HTTP 错误状态码,并使用容器自带的默认 HTML 错误页面模板渲染响应正文。voidsendError():(int sc, String msg),派发自定义错误状态码。向客户端发送指定的状态码,并将自定义的描述消息附加在默认错误模板中。
响应头设置
响应头用于指示客户端浏览器该如何解析接收到的数据、是否缓存以及是否执行页面跳转:
// 配置禁止浏览器缓存当前动态内容
resp.setHeader("Cache-Control", "no-cache");
// 设置响应内容格式与字符集编码
resp.setContentType("text/html;charset=UTF-8");voidsetHeader():(String name, String value),覆盖指定响应头。设置指定名称的 HTTP 响应头。如果该响应头已存在,其先前的值将被完全覆盖。voidaddHeader():(String name, String value),追加指定响应头。向指定名称的 HTTP 响应头追加新值,常用于允许多个同名字段的响应头(如Set-Cookie)。voidsetDateHeader():(String name, long date),覆盖时间戳响应头。将自 1970-01-01 算起的毫秒级时间戳转换为标准 HTTP 日期格式(RFC 1123)并写入响应头。常用响应头:
- 禁用缓存:同时设置
Cache-Control: no-cache、Pragma: no-cache、Expires: -1。 - 定时跳转:设置
Refresh: 3;url=/login.html,指示浏览器 3 秒后自动跳转至指定页面。
- 禁用缓存:同时设置
输出流机制
向客户端输出响应正文支持字符流与字节流两种途径:
// 获取字符输出流并写入响应正文
PrintWriter out = resp.getWriter();
out.println("Hello World");
// 显式刷新缓冲区将内容推送给客户端
out.flush();ServletOutputStreamgetOutputStream():(),获取二进制输出流。返回适用于写出原生字节数据的ServletOutputStream对象,主要用于文件下载、多媒体流传输或图片渲染等场景。PrintWritergetWriter():(),获取文本字符输出流。返回基于指定字符编码的PrintWriter字符打印流,适用于输出 HTML、JSON、XML 等纯文本数据。互斥约束:在同一次请求响应生命周期中,
getWriter()与getOutputStream()严禁同时调用,否则容器将抛出IllegalStateException。缓冲刷新:容器默认具备内置缓冲区(通常为 8KB)。当显式调用
flush()、缓冲区满或 Servlet 执行完毕时,数据正式提交并写入网络 Socket 通道。一旦响应被提交(isCommitted() == true),将无法再更改状态码和响应头。
编码处理
若服务端输出中文字符时未指定统一编码,容易因默认采用 ISO-8859-1 产生乱码:
// 必须在获取输出流之前声明内容类型与字符集
resp.setContentType("text/html;charset=UTF-8");
// 获取流后向客户端输出中文字符
resp.getWriter().println("你好,世界!");- 核心原理:
resp.setContentType("text/html;charset=UTF-8")同时完成了两项任务:指定 Tomcat 内部采用 UTF-8 编码将字符转换为字节流;在 HTTP 响应头中注入Content-Type: text/html;charset=UTF-8,指示浏览器以 UTF-8 规则进行解码渲染。 - 顺序约束:字符集配置代码必须放在调用
resp.getWriter()之前执行,否则输出流一旦创建,默认字符集即被绑定,后续配置无效。
请求重定向
请求重定向(Client Redirect) 是一种由客户端浏览器驱动的页面跳转机制。
当客户端向服务器发送请求时,当前 Servlet 并不直接返回最终目标内容,而是向浏览器返回一个重定向状态码(通常为 302 Found)以及一个包含目标地址的响应头(Location)。浏览器接收到该响应后,会自动以 GET 方式向该新地址重新发起一次独立的 HTTP 请求,最终获取目标资源并渲染呈现。
运作机制
请求重定向最本质的特征是两次独立的 HTTP 网络往返,跳转过程完全暴露在客户端层面。
如上图所示,重定向由客户端与服务器交互完成:
- 初次请求:客户端向服务器发起 URL1 资源请求。
- 告知重定向:服务器返回包含目标 URL2 的响应信息(包含 302 状态码与 Location 响应头)。
- 二次请求:客户端读取新地址,主动向 URL2 发起第二次 HTTP 请求。
- 最终响应:服务器定位到 URL2 资源并回写最终内容。


实现方式
在 Servlet 中,重定向主要通过 HttpServletResponse 提供的 API 来完成:
// 方式一:调用封装好的快捷 API 完成重定向
resp.sendRedirect(req.getContextPath() + "/success.html");
// 方式二:底层等价实现,手动设置状态码与响应头
resp.setStatus(302);
resp.setHeader("Location", req.getContextPath() + "/success.html");voidsendRedirect():(String location),发送重定向指令。向客户端发送临时重定向响应(HTTP 302 状态码),浏览器接收到后将自动向新的location地址发起全新请求。- 底层原理:两者在 HTTP 网络协议层面的效果完全相同。
路径规范
因为重定向是让浏览器重新发起连接,所以路径的基准解析由浏览器完成,而非服务器内部:
- 站内绝对路径:重定向到本项目的其他页面时,路径必须显式拼接应用上下文(Context Path),例如
req.getContextPath() + "/index.jsp"。如果误写成/index.jsp,浏览器会默认从服务器根域名(http://localhost:8080/index.jsp)发起请求,导致404 Not Found错误。 - 站外绝对路径:重定向完全支持跳转至外部站点,直接传入完整的 URL 即可,例如
resp.sendRedirect("[https://www.example.com](https://www.example.com)")。 - 受保护资源限制:重定向无法直接访问服务器的
/WEB-INF/目录,因为该目录受 Web 容器安全保护,禁止由外网或浏览器直接寻址。
典型应用
请求重定向在 Web 开发中最常用于表单重复提交防护(PRG 模式:Post-Redirect-Get)以及身份认证鉴权:
- PRG 模式:用户提交订单(POST 请求)后,若直接转发到成功页,用户刷新浏览器会导致表单二次提交。采用重定向到成功页(GET 请求),刷新操作仅会重复加载静态展示页。
- 登录鉴权:未登录用户访问受限资源时,过滤器或 Servlet 将请求重定向至登录页
login.html。
实战:文件下载重定向

需求分析与流程设计
本实战案例模拟基于 sendRedirect 实现文件下载入口引导与静态资源重定向分发:
- 页面重定向(入口引导):客户端访问下载入口 Servlet(如
/down)时,服务端经业务处理后,调用resp.sendRedirect(req.getContextPath() + "/down.html")发送 302 临时重定向,引导浏览器跳转至下载引导页面。此时浏览器地址栏显式变更为http://localhost:8080/servlet/down.html(如上图所示)。 - 资源重定向(触发下载):在
down.html页面中点击“下载天龙八部”超链接,请求发送给后端的DownServlet。Servlet 完成校验或日志记录后,再次调用resp.sendRedirect(req.getContextPath() + "/download/天龙八部.zip")将请求重定向至服务器静态资源目录下的目标压缩包文件,由浏览器直接向该地址发起二次 GET 请求,触发文件下载保存。
前端页面设计(down.html)
在 Web 应用根目录下创建 down.html,展示下载标题及下载超链接:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>下载文件</title>
</head>
<body>
<h1>下载文件</h1>
<!-- 点击超链接请求后端 Servlet 进行重定向分发 -->
<a href="downServlet">下载天龙八部</a>
</body>
</html>提示:此处
<a href="downServlet">采用相对路径,当页面位于http://localhost:8080/servlet/down.html时,浏览器会自动解析为http://localhost:8080/servlet/downServlet;亦可写作包含 Context Path 的站内绝对路径href="/servlet/downServlet"。
后端 Servlet 实现(DownServlet.java)
后端通过继承 HttpServlet,在 doGet 中利用 resp.sendRedirect 完成页面跳转与资源下载的重定向分发:
package com.hspedu.servlet;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
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(urlPatterns = {"/down", "/downServlet"})
public class DownServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String servletPath = req.getServletPath();
// 场景一:访问 /down 入口,重定向至前端静态下载展示页 down.html
if ("/down".equals(servletPath)) {
resp.sendRedirect(req.getContextPath() + "/down.html");
return;
}
// 场景二:在页面点击“下载天龙八部”链接(请求 /downServlet),重定向至实际资源下载路径
if ("/downServlet".equals(servletPath)) {
// 目标资源文件名(存放在 webapp/download/ 目录下)
String fileName = "天龙八部.zip";
// 对含中文的文件名进行 URL 编码,避免 HTTP Location 响应头非法字符报错
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
// 发送 302 重定向,指引浏览器直接向实际文件路径发起下载请求
resp.sendRedirect(req.getContextPath() + "/download/" + encodedFileName);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}- 显式拼接上下文路径:重定向是由客户端浏览器重新发起 HTTP 请求,因此站内重定向路径必须以
/开头并显式携带req.getContextPath()(即/servlet),否则浏览器会默认从服务器根域名寻址导致 404 错误。 - 中文 URL 转码:HTTP
Location响应头仅支持 ASCII 字符。若重定向路径中包含中文字符(如“天龙八部”),必须使用URLEncoder.encode()进行百分号编码转义,防止触发非法响应头字符异常。 - 业务解耦与前置控制:通过 Servlet 进行下载重定向,可以在实际下载前执行权限核验、下载计次、动态分流(如重定向至第三方 CDN)等逻辑,比在前端直接暴露静态文件链接更为安全灵活。
文件下载@
当需要服务端提供文件下载而非在浏览器内直接预览打开时,需结合响应头与二进制字节流实现:
// 设置响应头通知浏览器以附件形式保存并指定默认文件名
resp.setHeader("Content-Disposition", "attachment;filename=\"report.pdf\"");
// 设置响应格式为通用二进制数据流
resp.setContentType("application/octet-stream");
// 获取字节输出流写回二进制文件数据
ServletOutputStream out = resp.getOutputStream();Content-Disposition:声明为attachment会强制浏览器触发“文件下载保存”对话框,filename指定默认存储文件名。Content-Type:声明为application/octet-stream表示通用的二进制数据流,避免浏览器自动执行内联渲染(Inline View)。
执行流程
从客户端发起请求至响应完成经历以下标准步骤:
实例创建:Tomcat 容器接收 HTTP 连接,完成请求解析后,同步创建
HttpServletRequest与HttpServletResponse实例。逻辑调用:容器指派工作线程调用目标 Servlet 的
service()方法,并将 request 和 response 传递给开发者的doGet()或doPost()。数据组装:业务代码设置状态码、定义响应头,并通过字符流或字节流向底层输出缓冲区填充响应体数据。
提交响应:当输出缓冲区填满、开发者调用
flush()或 Servlet 方法执行结束时,容器触发响应提交(Commit),组装出标准 HTTP 格式报文。通道回写与销毁:底层网络组件将组装好的报文通过 TCP 通道写回客户端,随后容器将 response 对象销毁或归还对象池以备复用。
API: HttpServletResponse
状态码
voidsetStatus():(int sc),设置 HTTP 响应状态码。向响应状态行注入指定的标准 HTTP 状态码(如200、201、404等)。intgetStatus():(),获取当前响应状态码。返回当前响应对象中已挂载的 HTTP 状态码。
注意事项:
- 原规范中的
setStatus(int sc, String sm)方法因允许传入非标准状态描述文本(Reason Phrase),在 Servlet 2.1 之后已被废弃(Deprecated),严禁在新项目中使用。setStatus()必须在响应被正式提交(Committed)之前调用。若响应已提交,调用此方法将不会产生任何网络写出效果,且可能在容器日志中引发警告。- 建议直接引用
HttpServletResponse中预定义的整型常量(例如HttpServletResponse.SC_OK、HttpServletResponse.SC_NOT_FOUND),避免硬编码数字。
import jakarta.servlet.http.HttpServletResponse;
public class StatusCodeManager {
public static void configureStatus(HttpServletResponse response, boolean resourceCreated) {
if (resourceCreated) {
// 201 Created: 表示资源已被成功创建
response.setStatus(HttpServletResponse.SC_CREATED);
} else {
// 200 OK: 标准请求处理成功
response.setStatus(HttpServletResponse.SC_OK);
}
// 读取当前设置的状态码并记录
int currentStatus = response.getStatus();
System.out.println("当前响应状态码: " + currentStatus);
}
}响应头
voidsetHeader():(String name, String value),覆盖指定响应头。设置指定名称的 HTTP 响应头。如果该响应头已存在,其先前的值将被完全覆盖。voidaddHeader():(String name, String value),追加指定响应头。向指定名称的 HTTP 响应头追加新值,常用于允许多个同名字段的响应头(如Set-Cookie)。voidsetIntHeader():(String name, int value),覆盖整型响应头。将指定响应头的值设定为整型数字的字符串形式。若已存在则直接覆盖。voidaddIntHeader():(String name, int value),追加整型响应头。向指定响应头追加一个整型数值。voidsetDateHeader():(String name, long date),覆盖时间戳响应头。将自 1970-01-01 算起的毫秒级时间戳转换为标准 HTTP 日期格式(RFC 1123)并写入响应头。voidaddDateHeader():(String name, long date),追加时间戳响应头。将毫秒级时间戳转换为标准 HTTP 日期字符串并追加至指定头字段。booleancontainsHeader():(String name),检查响应头是否存在。判断当前响应是否已经设置了指定名称的响应头(字段名大小写不敏感)。StringgetHeader():(String name),获取首个响应头值。根据名称返回当前响应头中记录的第一个值。Collection<String>getHeaders():(String name),获取同名响应头集合。返回与指定头名称关联的所有值的不可变集合。Collection<String>getHeaderNames():(),获取全部响应头名称。返回当前响应对象中包含的所有响应头名称的集合。
注意事项:
- HTTP 响应头字段名称不区分大小写,但规范建议采用首字母大写的连字符标准写法(如
Content-Disposition、Cache-Control)。setHeader()与addHeader()的核心差异在于覆盖还是追加。对于只允许单个值的字段(如Content-Type、Location)必须使用setHeader();而对于允许逗号分隔或多行的头(如Cache-Control)可使用addHeader()。
import jakarta.servlet.http.HttpServletResponse;
import java.util.Collection;
public class ResponseHeaderManager {
public static void setupSecurityAndCacheHeaders(HttpServletResponse response) {
// 1. 覆盖方式写入安全防护头
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
// 2. 追加方式设置多值头
response.addHeader("Cache-Control", "no-cache");
response.addHeader("Cache-Control", "no-store");
// 3. 数值与日期辅助方法
response.setIntHeader("X-Rate-Limit-Remaining", 99);
response.setDateHeader("Expires", System.currentTimeMillis() + 3600_000L);
// 4. 检查与读取已注入的头
if (response.containsHeader("X-Frame-Options")) {
String frameOption = response.getHeader("X-Frame-Options");
System.out.println("Frame 配置: " + frameOption);
}
Collection<String> cacheControls = response.getHeaders("Cache-Control");
System.out.println("Cache-Control 规则数量: " + cacheControls.size());
Collection<String> allHeaderNames = response.getHeaderNames();
System.out.println("当前响应头总项数: " + allHeaderNames.size());
}
}内容元数据
voidsetContentType():(String type),设置内容媒体类型。设定发送给客户端的 MIME 类型(如application/json、text/html),同时可包含字符编码参数(如text/html;charset=UTF-8)。StringgetContentType():(),获取当前内容媒体类型。返回当前已配置的 MIME 类型字符串(若包含字符集则一并返回)。voidsetCharacterEncoding():(String charset),显式设定字符集编码。指定通过getWriter()输出字符数据时使用的字符编码格式(如UTF-8)。StringgetCharacterEncoding():(),获取当前字符集编码。返回响应正文输出所采用的字符集名称。voidsetContentLength():(int len),设置响应体长度。设置 HTTP 响应头中的Content-Length字段(适用于 2GB 以内的数据)。voidsetContentLengthLong():(long len),设置长整型响应体长度。用于指定超过 2GB( 字节)的超大响应体数据长度。
注意事项:
setContentType()与setCharacterEncoding()必须在调用getWriter()之前以及响应提交(Committed)之前生效。若在getWriter()已经实例化之后再更改编码,该修改会被容器直接忽略。setContentLength()不可在使用分块传输编码(Chunked Transfer Encoding)时随意设定。如果容器发现没有手动显式设定Content-Length且响应体超过了缓冲区大小,通常会自动切换为Transfer-Encoding: chunked模式发送。
import jakarta.servlet.http.HttpServletResponse;
public class ContentMetadataConfigurator {
public static void configureJsonMetadata(HttpServletResponse response, int payloadByteLength) {
// 显式指定编码方式(推荐在 getWriter 之前执行)
response.setCharacterEncoding("UTF-8");
// 设置标准 JSON 媒体类型
response.setContentType("application/json");
// 显式指定正文长度(非必要,但能提升客户端接收体验)
response.setContentLength(payloadByteLength);
// 获取并核验最终生效的元数据
String effectiveType = response.getContentType();
String effectiveEncoding = response.getCharacterEncoding();
System.out.printf("生效 Content-Type: %s | 字符集: %s\n", effectiveType, effectiveEncoding);
}
public static void configureLargeBinaryMetadata(HttpServletResponse response, long totalFileBytes) {
response.setContentType("application/octet-stream");
// 超过 2GB 的文件必须使用 setContentLengthLong
response.setContentLengthLong(totalFileBytes);
}
}输出流与字符流
ServletOutputStreamgetOutputStream():(),获取二进制输出流。返回适用于写出原生字节数据的ServletOutputStream对象,主要用于文件下载、多媒体流传输或图片渲染等场景。PrintWritergetWriter():(),获取文本字符输出流。返回基于指定字符编码的PrintWriter字符打印流,适用于输出 HTML、JSON、XML 等纯文本数据。
注意事项:
- 不可兼得限制:同一响应中不可混用
getOutputStream()和getWriter()。若先调用了getOutputStream(),后续代码再次调用getWriter()(反之亦然),容器将直接抛出IllegalStateException: getOutputStream() has already been called for this response。- 关闭流规范:一般情况下,Servlet 开发者无需在
finally块中手动调用out.close()。Servlet 容器在请求服务方法执行结束后,会自动刷新并关闭输出流。若过早手动显式close(),将导致后续 Filter 无法拦截或追加响应内容。
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
public class ResponseStreamHandler {
// 方式 A:基于字符流输出文本(JSON/HTML)
public static void writeTextResponse(HttpServletResponse response, String textPayload)
throws IOException {
response.setContentType("text/plain;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.print(textPayload);
// 注意:无需手动 writer.close(),交给容器生命周期管理
}
// 方式 B:基于字节流输出二进制数据(文件下载/图片)
public static void writeBinaryResponse(HttpServletResponse response, byte[] binaryData)
throws IOException {
response.setContentType("application/octet-stream");
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(binaryData);
outputStream.flush();
}
}缓冲机制
voidsetBufferSize():(int size),设定首选缓冲区大小。请求容器为响应正文分配指定大小的底层字节缓冲区。容器会分配一个至少为此大小的缓冲区。intgetBufferSize():(),获取实际缓冲区大小。返回当前响应底层实际分配的缓冲区大小(字节)。voidflushBuffer():(),强制刷入网络层。将缓冲区中当前缓存的所有内容立即强制写入底层网络套接字(Socket)。此调用将直接导致响应被标记为已提交(Committed)。voidresetBuffer():(),清空正文缓冲区。仅清除缓冲区中尚未写出的正文内容,但保留当前已设置的状态码和所有响应头。voidreset():(),重置整个响应。彻底清除缓冲区中的数据,同时重置所有的状态码和响应头为初始状态。booleanisCommitted():(),检测响应是否已提交。返回布尔值,指示是否已有 HTTP 状态行和部分头信息已经实际发送到底层网络。
注意事项:
setBufferSize()必须在有任何正文数据写入之前调用,否则将抛出IllegalStateException。- 当
isCommitted()返回true时,严禁调用reset()或resetBuffer(),否则会立即触发IllegalStateException: Cannot reset buffer - response has already been committed。- 一旦缓冲区累积的数据量超过了预设的
bufferSize,容器会自动隐式触发flushBuffer(),从而使响应进入已提交状态。因此在处理长文本或大数据流时需格外警惕状态提交的时机。
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class ResponseBufferController {
public static void processWithRollbackCapability(HttpServletResponse response) throws IOException {
// 1. 在写入前扩容缓冲区(例如分配 32KB)
response.setBufferSize(32 * 1024);
System.out.println("实际缓冲区大小: " + response.getBufferSize() + " 字节");
response.setContentType("text/plain;charset=UTF-8");
PrintWriter writer = response.getWriter();
try {
writer.print("正在处理复杂运算第一阶段...\n");
// 模拟业务异常发生
boolean businessErrorOccurred = true;
if (businessErrorOccurred) {
throw new RuntimeException("业务流水号计算失败");
}
// 成功时提交
response.flushBuffer();
} catch (Exception ex) {
// 2. 检查是否已被提交,若未提交则回滚缓冲区输出
if (!response.isCommitted()) {
response.reset(); // 清空正文并重置状态码及响应头
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("系统异常回滚: " + ex.getMessage());
} else {
System.err.println("响应已提前提交,无法执行重置操作!");
}
}
}
}页面重定向与错误派发
voidsendRedirect():(String location),发送重定向指令。向客户端发送临时重定向响应(HTTP 302 状态码),浏览器接收到后将自动向新的location地址发起全新请求。voidsendError():(int sc),派发标准错误状态码。向客户端发送指定的 HTTP 错误状态码,并使用容器自带的默认 HTML 错误页面模板渲染响应正文。voidsendError():(int sc, String msg),派发自定义错误状态码。向客户端发送指定的状态码,并将自定义的描述消息附加在默认错误模板中。
注意事项:
- 重定向 vs 转发(Forward):
sendRedirect:属于客户端行为。浏览器地址栏会发生改变,产生两次独立的 HTTP 请求,无法通过request.getAttribute()共享上文数据。支持跨域跳转到外部完整 URL。forward:属于服务端行为。浏览器地址栏不变,仅一次 HTTP 请求,内部共享同一个HttpServletRequest作用域。只能在当前 Web 应用内部进行转发。
- 代码不会隐式中断:调用
sendRedirect()或sendError()之后,当前方法后续的代码依然会继续向下执行。为避免出现逻辑混乱或再次写入数据引发异常,必须在调用后显式加上return;。- 若响应已被提交(
isCommitted() == true),再次调用sendRedirect()或sendError()将直接抛出IllegalStateException。
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class NavigationController {
public static void handleRedirectAndErrors(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String action = request.getParameter("action");
if ("external-docs".equals(action)) {
// 重定向至外部链接(注意:调用后必须显式 return 阻断后续代码)
response.sendRedirect("https://jakarta.ee/specifications/servlet/");
return;
}
if ("forbidden-resource".equals(action)) {
// 发送 403 错误并交由容器渲染错误页
response.sendError(HttpServletResponse.SC_FORBIDDEN, "访问受限:无权操作核心系统资源");
return;
}
// 正常业务流程
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("请求已通过导航校验");
}
}Cookie 管理
voidaddCookie():(Cookie cookie),添加客户端 Cookie。将指定的 Cookie 附加至 HTTP 响应头中(生成Set-Cookie标头),客户端接收后将在符合域名与路径规则的后续请求中自动回传该 Cookie。
注意事项:
addCookie()可被多次调用,每次调用都会向响应报文追加一个独立的Set-Cookie标头,而不是相互覆盖。- 安全属性设置:对于包含敏感会话信息的 Cookie,必须设置
setHttpOnly(true)(防御 XSS 攻击读取)以及setSecure(true)(限制仅在 HTTPS 下传输)。在较新的 Servlet 规范与主流容器中,还需配合配置SameSite属性以防止跨站请求伪造(CSRF)。- 时机限制:与设置响应头相同,
addCookie()必须在响应被正式提交(isCommitted() == false)前调用,否则添加的 Cookie 将无法输出至客户端。
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
public class CookieManagementHelper {
public static void issueAuthenticationCookie(HttpServletResponse response, String token) {
// 创建指定键值的 Cookie 对象
Cookie authCookie = new Cookie("AUTH_TOKEN", token);
// 配置生命周期:7 天(单位:秒);0 表示立即删除,-1 表示内存级临时会话
authCookie.setMaxAge(7 * 24 * 60 * 60);
// 作用域限定:仅对当前应用根路径及子路径生效
authCookie.setPath("/");
// 安全防护:禁止浏览器 JavaScript 脚本读取,仅在网络传输时暴露
authCookie.setHttpOnly(true);
// 传输安全:仅在 TLS/HTTPS 加密通道中传输
authCookie.setSecure(true);
// 将 Cookie 注入到响应对象中
response.addCookie(authCookie);
}
}URL 重写机制
StringencodeURL():(String url),普通链接会话编码。针对页面内超链接或表单提交的目标 URL,由容器自动判断客户端是否支持 Cookie;若不支持或会话刚建立,则在 URL 尾部追加;jsessionid=...会话编码。StringencodeRedirectURL():(String url),重定向链接会话编码。专门针对即将传入response.sendRedirect()的目标 URL 进行会话编码判定与改写。
注意事项:
- 原有的
encodeUrl(String url)与encodeRedirectUrl(String url)命名不符合 Java 驼峰规范,已被废弃,必须使用大写的encodeURL()与encodeRedirectURL()。encodeURL()具有智能判定能力:如果客户端请求本身已经携带了合法的 Cookie,容器会认为客户端具备 Cookie 存储能力,此时该方法将直接原样返回传入的 URL,而不会追加冗余的;jsessionid。- 重定向与页面跳转时,若使用了无 Cookie 会话方案,所有涉及页面跳转的 URL 必须全部经过
encodeURL()处理,否则只要出现一次漏转,客户端会话跟踪链就会彻底断裂。
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
public class UrlRewriterHelper {
public static String buildSessionAwareLink(HttpServletRequest request, HttpServletResponse response) {
// 确保 Session 已建立
HttpSession session = request.getSession(true);
session.setAttribute("USER_STATUS", "ACTIVE");
String targetPath = request.getContextPath() + "/user/profile";
// 如果客户端禁用了 Cookie,encodeURL 会将其转换为: /context/user/profile;jsessionid=...
return response.encodeURL(targetPath);
}
public static void redirectToProtectedPage(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String redirectTarget = request.getContextPath() + "/dashboard";
// 重定向专用编码判定
String safeRedirectUrl = response.encodeRedirectURL(redirectTarget);
response.sendRedirect(safeRedirectUrl);
}
}HTTP/2 协议拓展
PushBuildernewPushBuilder():(),创建服务器推送构建器。在基于 HTTP/2 协议且客户端未禁用 Server Push 的连接上生成构建器对象;若底层为 HTTP/1.1 或连接不支持,则返回null。
注意事项:
newPushBuilder()是 Servlet 4.0(Java EE 8 / Jakarta EE 8+)引入的特性。- 在调用该方法前必须进行非空检查。因为当客户端通过 HTTP/1.1 访问、或在 HTTP/2 设置帧中显式声明
SETTINGS_ENABLE_PUSH=0时,该方法必定返回null。- 行业规范演进提醒:虽然 Servlet 规范完整支持该接口,但由于现代前端构建体系(如资源哈希与缓存预热)成熟,且主流浏览器自 2022 年起已逐步停止对 HTTP/2 Server Push 的支持(推荐改用 103 Early Hints),在生产系统中使用时需充分评估客户端网络环境。
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.PushBuilder;
public class Http2PushHelper {
public static void pushCriticalAssets(HttpServletRequest request, HttpServletResponse response) {
// 获取 HTTP/2 服务端推送构建器
PushBuilder pushBuilder = response.newPushBuilder();
// 严格判空:若当前通道不支持或为 HTTP/1.1,则优雅降级
if (pushBuilder != null) {
pushBuilder.path("static/css/theme-dark.css")
.addHeader("content-type", "text/css")
.push();
pushBuilder.path("static/js/bundle-core.js")
.addHeader("content-type", "application/javascript")
.push();
System.out.println("HTTP/2 Server Push 资源宣告已派发");
}
}
}