作为 JavaWeb 的底层基石,Servlet 及周边组件的细节、原理、坑点直接决定了后端代码的稳定性。本文从底层原理、实战进阶、踩坑指南三个维度,把这些知识点讲透,覆盖企业开发的核心需求。
一、Servlet 配置:不止是 “贴名牌”,更是 “规则博弈”
Servlet 的配置核心是URL 映射规则——Tomcat 如何根据请求路径找到对应的 Servlet,这是极易踩坑的细节。
1. URL 映射的 3 种规则(及优先级)
Tomcat 的 URL 匹配遵循精确匹配 > 路径匹配 > 扩展名匹配的优先级,实战中必须明确:
- 精确匹配:路径完全一致,比如
@WebServlet("/user"),仅匹配/user; - 路径匹配:以
/*结尾,比如@WebServlet("/user/*"),匹配/user/1、/user/detail; - 扩展名匹配:以
.xxx结尾,比如@WebServlet("*.do"),匹配/login.do、/order.do; - 默认匹配:
@WebServlet("/"),匹配所有未被其他规则匹配的请求(注意:会覆盖 Tomcat 默认的静态资源处理器,导致 CSS/JS 无法访问)。
2. 进阶配置:提前加载 Servlet
默认情况下,Servlet 在第一次请求时才会实例化,但可以通过load-on-startup让 Servlet 在 Tomcat 启动时就加载(适合初始化耗时的组件,比如加载配置文件):
java
运行
// 注解版:loadOnStartup值越小,优先级越高(≥0)
@WebServlet(value = "/init", loadOnStartup = 1)
public class InitServlet extends HttpServlet {
@Override
public void init() throws ServletException {
// Tomcat启动时执行:加载全局配置、初始化连接池等
System.out.println("Tomcat启动时,我就初始化了!");
}
}
// web.xml版:
<servlet>
<servlet-name>InitServlet</servlet-name>
<servlet-class>com.yourpackage.InitServlet</servlet-class>
<load-on-startup>1</load-on-startup> <!-- 启动时加载 -->
</servlet>
3. 实战踩坑:URL 匹配冲突
如果同时配置了@WebServlet("/user/*")和@WebServlet("/user/detail"),请求/user/detail会匹配哪个?→ 优先匹配精确匹配的/user/detail,而非路径匹配的/user/*。
二、Servlet 生命周期:从 “出生” 到 “死亡” 的底层逻辑
Servlet 的生命周期由Tomcat 容器管理,核心是 “单例多线程”—— 一个 Servlet 类仅实例化一次,多请求共享同一个对象(因此 Servlet 类不能有线程不安全的成员变量)。
1. 生命周期的 4 个阶段(深化版)
| 阶段 | 触发时机 | 核心方法 & 作用 |
|---|---|---|
| 加载实例化 | Tomcat 启动(load-on-startup)或第一次请求 | 调用构造方法:创建 Servlet 对象(仅 1 次) |
| 初始化 | 实例化后立即执行 | 调用init(ServletConfig config):初始化资源(如连接池),ServletConfig可获取配置参数 |
| 服务 | 每次请求 | 调用service(HttpServletRequest req, HttpServletResponse resp):分发到doGet/doPost |
| 销毁 | Tomcat 关闭或应用卸载 | 调用destroy():释放资源(如关闭连接池) |
2. 进阶:通过 ServletConfig 获取配置参数
可以给 Servlet 配置专属参数,在init中通过ServletConfig获取:
java
运行
// 注解版:
@WebServlet(value = "/config", initParams = {
@WebInitParam(name = "maxCount", value = "100"),
@WebInitParam(name = "timeout", value = "5000")
})
public class ConfigServlet extends HttpServlet {
private int maxCount;
private int timeout;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config); // 必须调用父类方法,否则getServletConfig()会返回null
// 获取初始化参数
maxCount = Integer.parseInt(config.getInitParameter("maxCount"));
timeout = Integer.parseInt(config.getInitParameter("timeout"));
System.out.println("maxCount=" + maxCount + ", timeout=" + timeout);
}
}
3. 实战踩坑:Servlet 的线程安全
错误写法(线程不安全):
java
运行
@WebServlet("/unsafe")
public class UnsafeServlet extends HttpServlet {
// 成员变量:多线程共享,会被并发修改
private String username;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
username = req.getParameter("name");
// 模拟耗时操作,此时其他请求可能修改username
try { Thread.sleep(1000); } catch (InterruptedException e) {}
resp.getWriter().write("Hello " + username);
}
}
→ 正确做法:避免成员变量,用局部变量或ThreadLocal存储请求专属数据。
三、请求 & 响应:HTTP 协议的 “具象化”
HttpServletRequest/HttpServletResponse 是对 HTTP 协议的封装,底层是TCP 连接中的字节流——Tomcat 帮我们解析了 HTTP 报文,封装成对象。
1. 请求的进阶用法:处理复杂参数
(1)多值参数(复选框):getParameterMap
java
运行
// 前端表单:<input type="checkbox" name="hobby" value="篮球">篮球
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 获取多值参数(返回Map<String, String[]>)
Map<String, String[]> paramMap = req.getParameterMap();
String[] hobbies = paramMap.get("hobby");
if (hobbies != null) {
String hobbyStr = String.join(",", hobbies);
resp.getWriter().write("你的爱好:" + hobbyStr);
}
}
(2)请求体数据(JSON/XML):getReader
如果前端传的是 JSON(而非表单),需要读取请求体:
java
运行
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 读取请求体(JSON字符串)
BufferedReader reader = req.getReader();
String line;
StringBuilder json = new StringBuilder();
while ((line = reader.readLine()) != null) {
json.append(line);
}
System.out.println("前端传的JSON:" + json); // 如:{"name":"张三","age":20}
}
2. 响应的进阶用法:状态码 & 文件下载
(1)手动设置状态码
java
运行
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String id = req.getParameter("id");
if (id == null) {
resp.setStatus(400); // 400:请求参数错误
resp.getWriter().write("缺少id参数");
return;
}
// 其他逻辑...
}
(2)文件下载响应
java
运行
@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1. 设置响应头:告诉浏览器这是文件下载
resp.setContentType("application/octet-stream");
resp.setHeader("Content-Disposition", "attachment;filename=test.txt");
// 2. 读取服务器文件,写入响应流
ServletContext context = getServletContext();
InputStream in = context.getResourceAsStream("/WEB-INF/test.txt");
OutputStream out = resp.getOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
in.close();
}
}
3. 底层原理:请求 / 响应的生命周期
- 请求对象(HttpServletRequest):每个请求对应一个新的对象,请求结束后被 Tomcat 回收;
- 响应对象(HttpServletResponse):与请求一一对应,响应完成后(
out.close())不能再操作,否则会抛IllegalStateException。
四、转发 & 重定向:底层流程与场景抉择
这是面试高频题,核心是 **“一次请求” vs “两次请求”** 的底层差异。
1. 底层流程对比(可视化)
| 操作 | 流程 | 数据传递 |
|---|---|---|
| 转发(Forward) | 客户端→Tomcat→ServletA→Tomcat 内部转发→ServletB→Tomcat→客户端 | request 域(同一个请求对象) |
| 重定向(Redirect) | 客户端→Tomcat→ServletA→Tomcat 返回 302 + 新地址→客户端→Tomcat→ServletB→客户端 | Session/URL 参数(新请求对象) |
2. 实战场景抉择
- 用转发:需要传递大量数据(用 request 域)、跳转后地址栏不变(比如表单提交后跳转到结果页,避免刷新重复提交);
- 用重定向:表单提交成功后跳转(避免刷新重复提交)、跳转到外部网站、需要清空 request 域(比如登录成功后跳首页)。
3. 进阶:重定向的 URL 编码(解决中文乱码)
如果重定向的 URL 包含中文,需要编码:
java
运行
String name = "张三";
// 编码中文参数
String encodedName = URLEncoder.encode(name, "UTF-8");
// 重定向
resp.sendRedirect(req.getContextPath() + "/index.jsp?name=" + encodedName);
// 接收时解码:
String name = URLDecoder.decode(req.getParameter("name"), "UTF-8");
五、过滤器:“链式拦截” 的执行逻辑与高级用法
过滤器(Filter)是AOP 思想的底层实现,核心是 “对请求 / 响应进行预处理 / 后处理”。
1. 过滤器链的执行顺序
过滤器链的执行顺序由配置顺序决定:
- web.xml 版:按
<filter-mapping>的配置顺序执行; - 注解版:按类名字母顺序执行(比如
AFilter比BFilter先执行)。
代码示例(过滤器链):
java
运行
// Filter1:
@WebFilter("/*")
public class Filter1 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Filter1:请求前处理");
chain.doFilter(request, response); // 放行到下一个过滤器/Servlet
System.out.println("Filter1:响应后处理");
}
}
// Filter2:
@WebFilter("/*")
public class Filter2 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Filter2:请求前处理");
chain.doFilter(request, response);
System.out.println("Filter2:响应后处理");
}
}
执行结果:
plaintext
Filter1:请求前处理
Filter2:请求前处理
(Servlet处理逻辑)
Filter2:响应后处理
Filter1:响应后处理
2. 高级用法:防止 XSS 攻击的过滤器
XSS 攻击是前端注入恶意脚本,过滤器可以对请求参数进行转义:
java
运行
@WebFilter("/*")
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 包装request,重写getParameter方法
XssHttpServletRequestWrapper wrapper = new XssHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrapper, response);
}
// 自定义Request包装类,转义参数
static class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return value == null ? null : escape(value);
}
// 转义HTML特殊字符
private String escape(String value) {
value = value.replaceAll("<", "<");
value = value.replaceAll(">", ">");
return value;
}
}
}
3. 实战踩坑:过滤静态资源
如果过滤器配置了/*,会过滤 CSS/JS/ 图片等静态资源,导致页面样式失效。解决方法:在过滤器中排除静态资源:
java
运行
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String path = req.getRequestURI().toLowerCase();
// 排除静态资源
if (path.endsWith(".css") || path.endsWith(".js") || path.endsWith(".png")) {
chain.doFilter(request, response);
return;
}
// 其他过滤逻辑...
}
六、监听器:“事件驱动” 的全场景覆盖
监听器(Listener)是观察者模式的实现,覆盖 Web 应用的所有核心事件。
1. 常用监听器分类
| 监听器类型 | 作用场景 | 核心方法示例 |
|---|---|---|
| ServletContextListener | 应用启动 / 销毁时执行(加载配置、初始化连接池) | contextInitialized(ServletContextEvent e) |
| ServletRequestListener | 每个请求的创建 / 销毁时执行(记录请求日志) | requestInitialized(ServletRequestEvent e) |
| HttpSessionListener | Session 创建 / 销毁时执行(统计在线人数) | sessionCreated(HttpSessionEvent e) |
| HttpSessionAttributeListener | Session 属性增 / 删 / 改时执行(监控用户状态) | attributeAdded(HttpSessionBindingEvent e) |
2. 实战:用 ServletContextListener 加载全局配置
应用启动时加载config.properties文件到 ServletContext 域:
java
运行
@WebListener
public class ConfigListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
// 读取WEB-INF下的配置文件
InputStream in = context.getResourceAsStream("/WEB-INF/config.properties");
Properties props = new Properties();
try {
props.load(in);
// 存到ServletContext域(全局共享)
context.setAttribute("config", props);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 其他Servlet中获取配置:
Properties props = (Properties) getServletContext().getAttribute("config");
String dbUrl = props.getProperty("db.url");
七、Cookie&Session:状态管理的 “底层逻辑与坑点”
HTTP 是无状态协议,Cookie 和 Session 是解决 “状态丢失” 的核心方案,但二者的安全、性能、分布式问题是企业开发的重点。
1. Cookie 的底层限制与安全
Cookie 的本质是 “客户端浏览器存储的文本文件”,存在以下限制:
- 大小限制:每个 Cookie≤4KB;
- 数量限制:每个域名下≤20 个 Cookie(不同浏览器略有差异);
- 跨域限制:Cookie 默认不跨域(可通过
setDomain设置主域名共享,比如setDomain(".example.com")让a.example.com和b.example.com共享); - 安全限制:
setHttpOnly(true):禁止 JS 读取 Cookie,防止 XSS 攻击;setSecure(true):仅在 HTTPS 协议下传输 Cookie,防止明文泄露。
2. Session 的底层实现与分布式问题
Session 的本质是 “服务器端的内存对象”,通过Cookie 中的 JSESSIONID关联客户端:
plaintext
客户端请求 → Tomcat生成Session(内存中)→ 向客户端写Cookie:JSESSIONID=XXXX → 客户端后续请求携带JSESSIONID → Tomcat根据JSESSIONID找到对应的Session
(1)Session 的超时配置
- web.xml 版:
xml
<session-config> <session-timeout>30</session-timeout> <!-- 单位:分钟 --> </session-config> - 代码版:
java
运行
HttpSession session = req.getSession(); session.setMaxInactiveInterval(1800); // 单位:秒(30分钟)
(2)分布式 Session 问题
如果项目部署在多台 Tomcat(集群),默认情况下 Session 不共享(每台 Tomcat 的 Session 是独立的),导致用户在 A 服务器登录后,访问 B 服务器又需要重新登录。
解决方案:
- Session 复制:Tomcat 集群开启 Session 复制(性能低,仅适用于小规模集群);
- Redis 共享 Session:将 Session 序列化后存储到 Redis(主流方案,比如 Spring Session 框架);
- Token 替代 Session:用 JWT 等 Token 机制,完全脱离 Session。
3. 实战踩坑:Cookie 中文乱码
Cookie 不支持直接存储中文,需要编码:
java
运行
// 编码:
String value = URLEncoder.encode("中文内容", "UTF-8");
Cookie cookie = new Cookie("key", value);
resp.addCookie(cookie);
// 解码:
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("key".equals(cookie.getName())) {
String value = URLDecoder.decode(cookie.getValue(), "UTF-8");
}
}
}
八、JSP:从 “翻译为 Servlet” 到 “现代替代方案”
JSP 的本质是 “Servlet 的模板”——Tomcat 会将 JSP 文件翻译为.java 文件,再编译为.class 文件执行。
1. JSP 的翻译过程(可视化)
比如index.jsp:
jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
Hello <%= request.getParameter("name") %>
</body>
</html>
Tomcat 会将其翻译为index_jsp.java,核心代码是:
java
运行
public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase {
public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType("text/html;charset=UTF-8");
javax.servlet.jsp.JspWriter out = pageContext.getOut();
out.write("<html>\n<body>\n Hello ");
out.print( request.getParameter("name") );
out.write("\n</body>\n</html>");
}
}
2. JSP 的 9 大内置对象(自动创建,直接使用)
| 内置对象名 | 类型 | 作用 |
|---|---|---|
| request | HttpServletRequest | 请求对象 |
| response | HttpServletResponse | 响应对象 |
| session | HttpSession | 会话对象 |
| application | ServletContext | 应用全局对象 |
| pageContext | PageContext | 页面上下文(域对象,作用域最小) |
| out | JspWriter | 输出流(替代 resp.getWriter ()) |
| page | Object | 当前 JSP 对应的 Servlet 对象(极少用) |
| config | ServletConfig | Servlet 配置对象 |
| exception | Throwable | 异常对象(仅在page errorPage="true"时可用) |
3. 现代替代方案:模板引擎
JSP 的缺点是 “前后端代码混编”、“性能低”,现在企业开发更多用Thymeleaf(SpringBoot 默认)、Freemarker 等模板引擎,比如 Thymeleaf 的语法:
html
预览
<!-- Thymeleaf替代JSP的EL表达式 -->
<div th:text="${user.name}">用户名</div>
<ul>
<li th:each="item : ${list}" th:text="${item}"></li>
</ul>
总结:JavaWeb 的 “底层逻辑” 与 “实战落地”
Servlet 及周边组件是 JavaWeb 的 “地基”——SpringMVC 的@Controller本质是 Servlet 的封装,SpringBoot 的自动配置本质是简化了 web.xml/ServletConfig 的配置。
掌握本文的原理、进阶用法、踩坑指南,不仅能独立开发传统 JavaWeb 项目,更能理解框架的底层逻辑,在遇到问题时快速定位根因。
最后,给新手的建议:先写原生 Servlet 项目,再用框架—— 框架是 “工具”,底层原理是 “内功”,内功扎实才能用好工具。

1508





