引子:Web世界的“工业革命”
在Web开发的“黑暗时代”,一种名为CGI (通用网关接口) 的技术统治着动态内容的生成。它的工作模式简单粗暴:每当服务器收到一个动态请求,就启动一个全新的操作系统进程来处理,处理完毕后立即销毁。这就像每次点外卖,平台都为你新开一家饭店,做完你的菜后就立刻倒闭。这种模式在高并发下,服务器资源会迅速耗尽,性能极其低下。
Java Servlet的诞生,正是为了终结这场混乱,它在Web世界掀起了一场“工业革命”。Servlet用轻量级的Java线程取代了重量级的操作系统进程,通过实例常驻内存、多线程处理请求的高效模式,将Web应用的性能和可扩展性提升到了一个全新的高度。
所有现代Java Web框架,无论它们看起来多么华丽和高级(如Spring MVC),其最底层的“发动机”依然是Servlet。因此,理解Servlet,就是理解所有Java Web应用的起点和心跳。这份指南,将带你从零开始,彻底解剖这台强大的“发动机”。
第一部分:Servlet的核心本质与生命周期
1. Servlet是什么?
- 定义: Servlet (Server Applet) 并非一个具体的程序,而是由Java官方制定的一套Web技术规范(一组Java接口)。
- 角色: 它是一个运行在Web容器/Servlet容器(如Tomcat, Jetty)内部的Java组件,负责接收和响应来自客户端的HTTP请求。
- 核心优势:
- 高性能: 容器启动时创建Servlet实例并使其常驻内存。对于每个请求,容器只创建一个轻量级的线程来处理,而不是重量级的进程。这使得Servlet能够轻松应对高并发场景。
- 平台无关: 一次编写,可以部署在任何实现了Servlet规范的Web容器上。
2. Servlet的生命周期 (The Lifecycle)
Servlet从诞生到消亡,其整个生命由Web容器严格管理。理解这个过程至关重要。
一个Servlet的核心生命周期由javax.servlet.Servlet
接口中的三个方法定义:
-
init(ServletConfig config)
- 初始化阶段 (只执行一次)- 何时调用?:在Servlet实例第一次被创建时调用(通常是第一次被访问或服务器启动时)。
- 作用: 执行一次性的、昂贵的初始化操作。例如:加载配置文件、创建数据库连接池、初始化缓存等。
- 类比: 相当于一个类的构造函数,但它是在Web容器的环境下被调用的,可以获取到容器提供的配置信息。
-
service(ServletRequest req, ServletResponse res)
- 服务阶段 (可执行无数次)- 何时调用?:每当有一个HTTP请求匹配到这个Servlet时,容器就会在一个新的线程中调用这个方法。
- 作用: 这是Servlet的心脏,所有业务逻辑都在这里处理。它负责从
ServletRequest
中读取请求信息,处理后通过ServletResponse
将响应内容写回浏览器。 - 线程安全警告: 由于多个线程会同时访问同一个Servlet实例,因此
service
方法(以及它调用的doGet
/doPost
)必须是线程安全的。严禁在Servlet类中定义可被修改的成员变量(实例变量)来存储与特定请求相关的数据,否则会产生数据错乱。
-
destroy()
- 销毁阶段 (只执行一次)- 何时调用?:当Web应用被卸载或服务器关闭时,容器会调用此方法。
- 作用: 用于释放
init
方法中创建的资源。例如:关闭数据库连接池、停止后台定时任务等,确保应用的优雅关闭。
生命周期图解:(图来源:菜鸟教程)
第二部分:Servlet实战:编写与部署
2.1 环境与依赖准备
- IDE: IntelliJ IDEA Ultimate
- Web容器: Apache Tomcat
- Maven依赖 (
pom.xml
):<dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>5.0.0</version> <!-- 注意版本号也变了 --> <scope>provided</scope> </dependency>
2.2 HttpServlet
:更方便的起点
通常,我们不直接实现Servlet
接口,而是继承javax.servlet.http.HttpServlet
这个抽象类。它已经帮我们实现了service
方法,并根据HTTP请求的类型(GET, POST, PUT, DELETE等)将请求分发到对应的doXxx()
方法。我们只需要重写我们关心的doGet()
或doPost()
即可。
2.3 动手实践:一个功能完备的HelloServlet
【操作】: 创建一个HelloServlet.java
。这份代码包含了生命周期方法、GET/POST处理、参数获取和响应构建。
package com.example.servlet;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
// 【核心注解】@WebServlet("/hello")
// Servlet 3.0+规范,用注解替代了在web.xml中配置<servlet>和<servlet-mapping>。
// 它告诉Tomcat:当浏览器访问路径为 "/hello" 时,就由这个Servlet来处理。
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config); // 必须调用父类的init
System.out.println("--- HelloServlet 的 init() 方法被调用了!(只在第一次访问时出现) ---");
}
// 处理GET请求
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("--- HelloServlet 的 doGet() 方法被调用了! ---");
// 1. 从HttpServletRequest中获取请求信息
String clientName = req.getParameter("name"); // 获取URL查询参数, 如 /hello?name=Tom
if (clientName == null || clientName.isEmpty()) {
clientName = "Guest";
}
// 2. 设置HttpServletResponse的响应头
// 告诉浏览器,我将返回的是HTML格式的内容,并且使用UTF-8编码
resp.setContentType("text/html;charset=UTF-8");
// 3. 获取一个用于向浏览器输出内容的 PrintWriter 对象
PrintWriter out = resp.getWriter();
// 4. 生成HTML响应内容
out.println("<html><head><title>Hello Servlet</title></head><body>");
out.println("<h1>你好</h1>" + clientName);
out.println("<p>这是由Servlet在服务器端动态生成的内容。</p>");
out.println("<p>服务器当前时间是: " + new Date() + "</p>");
out.println("</body></html>");
}
// 处理POST请求
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("--- HelloServlet 的 doPost() 方法被调用了! ---");
// 为了处理POST请求体中可能存在的中文乱码,必须在获取任何参数前设置
req.setCharacterEncoding("UTF-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("<h1>POST 请求已收到</h1>");
out.println("<p>用户名: " + username + "</p>");
out.println("<p>密码: " + password + "</p>");
}
@Override
public void destroy() {
System.out.println("--- HelloServlet 的 destroy() 方法被调用了!(在服务器关闭时出现) ---");
}
}
2.4 部署与运行
- 配置Tomcat: 在IDEA中配置好本地的Tomcat服务器。
- 部署项目: 将你的Web应用打包成WAR文件(或直接以exploded形式)部署到Tomcat。
- 启动Tomcat并访问
http://localhost:8080/你的项目名/hello?name=World
。
第三部分:核心API与数据共享机制
3.1 HttpServletRequest
& HttpServletResponse
-
request
(请求对象): 封装了所有来自客户端的HTTP请求信息。- 获取参数:
getParameter(String name)
,getParameterValues(String name)
- 获取请求头:
getHeader(String name)
- 获取路径信息:
getRequestURI()
,getContextPath()
,getServletPath()
- 获取请求方法:
getMethod()
- 数据共享 (请求域):
setAttribute(String, Object)
,getAttribute(String)
- 获取参数:
-
response
(响应对象): 用于构建并向客户端发送HTTP响应。- 设置响应类型:
setContentType(String type)
(极其重要!) - 获取输出流:
getWriter()
(用于文本),getOutputStream()
(用于二进制) - 设置响应头:
setHeader(String, String)
- 页面跳转:
sendRedirect(String location)
- 设置响应类型:
3.2 三大作用域:Web应用的“数据容器”
在一个Web应用中,数据可以在三个不同的范围内共享和传递。
-
ServletContext
(应用域)- 生命周期: 整个Web应用运行期间。
- 范围: 全局唯一,所有用户、所有请求共享。
- 作用: 存放全局配置、共享资源(如数据库连接池)、网站计数器等。
- 动手实践:网站访问计数器
修改HelloServlet.java
的doGet
方法:
为了在服务器启动时初始化计数器,我们可以使用监听器(见第四部分)。// ...在doGet方法内... ServletContext context = req.getServletContext(); Integer counter = (Integer) context.getAttribute("hitCounter"); // 第一次访问时,counter为null if (counter == null) { counter = 1; } else { counter++; } context.setAttribute("hitCounter", counter); out.println("<p>本站总访问量 (ServletContext): " + counter + "</p>"); // ...
-
HttpSession
(会话域)- 生命周期: 从用户第一次访问开始,到会话超时或手动
invalidate()
结束。 - 范围: 每个用户(浏览器会话)独享。
- 作用: 跟踪单个用户的状态,如登录信息、购物车。
- 底层原理: 基于Cookie(
JSESSIONID
)。 - 动手实践:实现一个简单的会话级计数器
修改HelloServlet.java
的doGet
方法:// ...在doGet方法内... HttpSession session = req.getSession(); // 获取或创建session Integer sessionCounter = (Integer) session.getAttribute("sessionCounter"); if (sessionCounter == null) { sessionCounter = 1; } else { sessionCounter++; } session.setAttribute("sessionCounter", sessionCounter); out.println("<p>您的会话访问次数 (HttpSession): " + sessionCounter + "</p>"); out.println("<p>您的Session ID: " + session.getId() + "</p>"); // ...
- 生命周期: 从用户第一次访问开始,到会话超时或手动
-
HttpServletRequest
(请求域)- 生命周期: 从一次HTTP请求到达服务器开始,到该请求的响应发送完毕结束。生命周期最短。
- 范围: 仅在同一次请求的处理链中有效。
- 作用: 在请求转发过程中,从一个Servlet向另一个Servlet或JSP传递数据。
3.3 请求转发 vs 重定向
-
请求转发 (Forward): 服务器内部的“内部交接”。地址栏不变,一次请求,共享
request
数据。- 动手实践:Servlet转发给另一个Servlet
- 创建
DataServlet.java
:@WebServlet("/data") public class DataServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setAttribute("sharedMessage", "这是来自DataServlet的机密信息"); System.out.println("DataServlet: 数据已准备好,准备转发..."); req.getRequestDispatcher("/display").forward(req, resp); } }
- 创建
DisplayServlet.java
:@WebServlet("/display") public class DisplayServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String message = (String) req.getAttribute("sharedMessage"); resp.getWriter().println("<h1>显示页面</h1><p>收到的共享数据: " + message + "</p>"); } }
- 测试: 浏览器访问
/data
,地址栏不变,但看到的是/display
的内容。
- 创建
- 动手实践:Servlet转发给另一个Servlet
-
重定向 (Redirect): 通知浏览器的“外部跳转”。地址栏改变,两次请求,
request
数据不共享。- 动手实践:处理完POST后重定向
在HelloServlet
的doPost
方法末尾,添加这行代码来替代原有的输出:// ...在doPost方法末尾... // 重定向到 /hello 路径 (会触发doGet) resp.sendRedirect(req.getContextPath() + "/hello?name=" + java.net.URLEncoder.encode(username, "UTF-8"));
req.getContextPath()
获取应用上下文路径,URLEncoder
对中文参数编码,都是良好实践。
- 动手实践:处理完POST后重定向
第四部分:过滤器 (Filter) 与监听器 (Listener)
4.1 过滤器 (Filter):请求的“安检门”
- 作用: 实现AOP(面向切面编程),对请求和响应进行统一的预处理和后处理。
- 企业级应用: 全局编码、权限校验、日志记录、数据压缩。
- 动手实践:创建一个全局编码过滤器
import jakarta.servlet.*; // 注意包名变更
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class EncodingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("--- EncodingFilter: 拦截到请求,设置编码 ---");
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
System.out.println("--- EncodingFilter: 响应已处理完毕 ---");
}
// 建议显式实现空的init()和destroy()方法
@Override
public void init(FilterConfig config) throws ServletException {
// 初始化代码(如果有)
}
@Override
public void destroy() {
// 清理资源代码(如果有)
}
}
4.2 监听器 (Listener):Web应用生命周期的“事件响应器”
- 作用: 在Web应用的关键生命周期节点上执行代码。
- 企业级应用: 应用启动时初始化数据库连接池、定时任务等重量级资源。
- 动手实践:使用
ServletContextListener
初始化资源
import jakarta.servlet.ServletContextEvent; // 注意包名变更
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
@WebListener
public class AppLifecycleListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("========= Web应用启动: contextInitialized() =========");
// 这里是初始化数据库连接池、Spring容器等的最佳位置
sce.getServletContext().setAttribute("hitCounter", 0);
System.out.println("全局访问计数器已初始化。");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("========= Web应用关闭: contextDestroyed() =========");
// 这里是释放资源、关闭连接池的最佳位置
}
}
总结
学完这份终极指南,你对Servlet的理解已经达到了一个非常深入的层次。你不仅掌握了Servlet如何处理单个请求的基础,更重要的是,你构建了一个完整的Java Web架构知识图谱:
- 你知道了如何通过三大作用域在Web应用的不同层面管理数据。
- 你明白了转发和重定向的本质区别和适用场景。
- 你学会了使用过滤器来实现横切关注点(如权限、编码),让代码更整洁。
- 你懂得了如何利用监听器在应用启动时加载和初始化关键服务。
这些知识,是理解Spring、Spring MVC乃至整个Java EE生态系统的坚实基础。