努力有时候也战胜不了天分,但至少能让别人看得起你。
——《七龙珠》
手动创建一个Maven项目
由于要学习Java中的Servlet,Filter,Listener等组件需要下载依赖,所以我们需要在构建工具中进行演示和学习。本文使用Maven项目作为示例。
创建文件夹结构
mkdir -p myapp/src/main/java //创建文件结构
cd myapp //进入项目
手动创建一个简单的 pom.xml
<!-- 文件: pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>1.0-SNAPSHOT</version>
</project>
这样我们就创建好了一个最简单maven项目。
Servlet
Servlet 是JavaWeb技术的核心组件,本质上就是运行在服务器端的小程序,用于处理客户端(如浏览器)的请求,生成动态响应。Servlet主要用于处理Http请求,实现动态网页内容生成。
Servlet 生命周期
Servlet的生命周期由Servlet容器(如Tomcat)管理,主要有以下几个阶段:
Servlet容器:Web服务器,用于接收HTTP请求并调用你写的Servlet 代码处理请求。
初始化 init() —只调用一次
- 在Servlet第一次被访问时,容器会创建一个Servlet实例,然后调用它的
init()方法。 - 通常用来做资源的初始化操作、比如数据库连接、读取配置文件等。
@Override
public void init() throws ServletException {
System.out.println("Servlet 初始化:init()");
}
注:只有一个实例,init()只调用一次
请求处理 service()—每次请求都会调用
- 每次有客户端请求Servlet,Tomcat就会调用Servlet实例的
service()方法 service()实例会自动判断请求类型(GET/POST),然后分发给对应的doGet() 或 doPost()
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("处理 GET 请求");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("处理 POST 请求");
}
注:此处我们先暂时了解大概流程,无需关心代码实际实现
销毁 destroy()—只调用一次
- 当Tomcat关闭、或Servlet被卸载(如热更新时),容器会调用
destroy()方法 - 通常用来释放资源,如关闭数据库连接,停止线程等。
@Override
public void destroy() {
System.out.println("Servlet 被销毁:destroy()");
}
补充说明
- 一个Servlet类只被实例化一次(单例),多个请求公用一个实例,线程不安全。
- Servlet生命周期由容器自动管理,你无需手动调用这些方法
- SpringMVC的低层DispatchServlet也是一个Servlet,生命周期和你写的一样。
部署Tomcat
Apache 基金会开发的免费、开源、轻量级 Java Web 容器 ,支持 Servlet 和 JSP 规范
- SpringBoot + SpringMVC 中内置Tomcat,无需配置容器,直接Main方法运行。
首先我们在官网下载安装TomCat:Apache Tomcat® - Welcome!
安装后记住安装路径,后面要用。
然后我们在VScode中安装扩展:

这个扩展帮助我们管理和启动Tomcat。
安装完成后,我们在VScode的侧边栏中的SERVICE中找到这个扩展(注意在资源管理器栏的最下面):

我们右键这个服务选择:Creat New Server
现在我们查看VScode的最上面,询问我们是否需要下载server,由于我们之前已经手动下载过Tomcat,所以选择No。
然后我们在弹出的窗口中选择之前安装的Tomcat的文件夹,来到这个页面:

不用更改配置,直接滑倒最下面点击Finish即可。
现在我们看到扩展中已经托管了Tomcat:

现在我们创建项目结构如下:
servlet-lifecycle-demo/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/LifecycleServlet.java
pom.xml文件如下:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>servlet-lifecycle-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>servlet-lifecycle-demo</finalName>
<plugins>
<!-- Maven war 插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.1</version>
</plugin>
</plugins>
</build>
</project>
LifecycleServlet.java 文件内容如下:
package com.example;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
@WebServlet("/life")
public class LifecycleServlet extends HttpServlet {
public LifecycleServlet() {
System.out.println("🚀 构造方法:Servlet 实例被创建");
}
@Override
public void init() throws ServletException {
System.out.println("🔧 init():Servlet 被初始化");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("📥 doGet():处理 GET 请求");
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("<h2>你好,这是 Servlet 生命周期演示</h2>");
}
@Override
public void destroy() {
System.out.println("🧹 destroy():Servlet 被销毁");
}
}
然后我们在终端中输入:
mvn clean package
好,现在我们只需要右键Tomcat选择:add Deployment

然后选择File:

选择我们刚刚编译好的wer文件:

最后选择No:

我们就将刚刚编写好的Servlet加入了Tomcat容器中。
右键Tomcat服务选择Start Server,服务开始运行。此时打印的信息只有Tomcat的启动信息,Servlet还没有被创建。

然后我们浏览器访问:http://localhost:8080/servlet-lifecycle-demo/life
看到servlet实例被创建,然后调用了一次GET请求,

之后再次访问页面,也不会在创建新的Servlet实例,而是一直调用同一个实例的doGet方法

最后我们Stop Server:

我们可以在日志中找到Servlet销毁函数调用是打印的日志。
Servlet Mapping
Servlet的作用是将客户端发送的请求URL路径与特定的Servlet 类关联起来。当接收到符合某个映射规则的请求时,它会将请求交给对应的Servlet来处理。
使用 @WebServlet注解配置映射
在我们上面的代码中,我们使用
@WebServlet("/life")
public class LifecycleServlet extends HttpServlet
进行注解配置。这里的”/life” 就是一个URL模式(URL pattern), 当用户访问路径 /life 时,Tomcat就会将这个请求交给 LifecycleServlet的实例来处理。
-
多个URL模式:可以为一个Servlet映射多个URL模式
@WebServlet({"/life","/mylife"})这样当访问多个路径中的任意一个路径时,都会交给
LifecycleServlet实例进行处理 -
指定
name:可以为Servlet指定一个名称@WebServlet(name = "lifecycleServlet", urlPatterns = {"/life"}) public class LifecycleServlet extends HttpServlet { // ... your code ... }在注解映射时,name属性通常不是必须的,但是在web.xml中,servlet-name属性是必须的,所以这里介绍一下。
使用 web.xml配置映射(传统方式)
在web应用的 WEB-INF目录下,会有一个web.xml文件。我们也可以通过这个文件进行Servlet配置和Servlet映射。
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>lifecycleServlet</servlet-name>
<servlet-class>com.example.LifecycleServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>lifecycleServlet</servlet-name>
<url-pattern>/life</url-pattern>
</servlet-mapping>
</web-app>
<servlet>用于声明一个Servlet,通过<servlet-name>指定名称(必须),<servlet-class>指定Servlet类的完整限定名。<servlet-mapping>元素用于将一个Servlet名称<servlet-name>映射到一个URL模式<url-pattern>
URL模式的类型
- 精确匹配:以
/开头,例如/life。只用当请求的路径与此路径完全匹配时,Servlet才会被调用。 - 路径匹配:以
/开头,以/*结尾,例如/admin/*。任何以/admin/开头的请求都会被匹配到这个Servlet。 - 扩展名匹配:以
*.开头,例如*.do。任何以.do结尾的请求都会被匹配到这个Servlet - 默认Servlet:只有一个
/作为URL模式,他会处理所有其他Servlet映射都无法匹配的请求。
Servlet特性
Servlet的线程安全性:
- Servlet实例是单例的,但是对于每个请求,Servlet容器都会创建一个新的线程来执行Servlet的
service()方法(doGet()或doPost()) - Servlet需要考虑线程安全性。避免在Servlet的实例变量中存储共享的可变状态,或者采取同步措施来保护共享资源。
Servlet的配置(ServletConfig):
- ServletConfig 对象代表Servlet的配置信息,每个servlet都有自己的ServletConfig对象。
- 我们可以在
web.xml(或使用@WebInitParam注解)为Servlet配置初始化参数,并通过ServletConfig的getInitParameter(String name)方法获取。
一个小DEMO
- 使用ServletConfig获取初始化参数:
<servlet>
<servlet-name>helloConfig</servlet-name>
<servlet-class>com.example.HelloConfig</servlet-class>
<init-param>
<param-name>exampleParam</param-name>
<param-value>Hello from config!</param-value>
</init-param>
</servlet>
public class HelloConfig extends HttpServlet {
private String exampleParam;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
exampleParam = config.getInitParameter("exampleParam");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("<h2>" + exampleParam + "</h2>");
}
}
- 使用
@WebInitParam注解
@WebServlet(urlPatterns = "/hello")
@WebInitParam(name = "exampleParam", value = "Hello from annotation!")
public class HelloAnnotation extends HttpServlet {
private String exampleParam;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
exampleParam= config.getInitParameter("exampleParam");
System.out.println("Greeting from annotation: " + exampleParam);
}
// ... doGet method ...
}
无论使用注解还是web.xml在Servlet类中获取初始化参数的方法都一样。只是注册参数的方法不同。
Servlet API详解
Servlet API 是构建Java Web 应用的核心。它定义了Servlet容器和你的Servlet之间的契约。
HttpServletRequest 接口
HttpServletRequest 对象代表了客户端发送给Servlet的HTTP请求。通过这个对象,你可以获取到请求的所有信息。
常用方法:
获取参数时,如果没有设置该参数,则会得到
null
getParameter(String name): 获取指定名称的请求参数的值(返回String)。如果参数不存在或有多个同名参数,行为可能不确定。getParameterValues(String name): 获取指定名称的所有请求参数的值(返回String[])。getParameterMap(): 获取所有请求参数的Map<String, String[]>。getHeader(String name): 获取指定名称的请求头的值(返回String)。getHeaders(String name): 获取指定名称的所有请求头的值(返回Enumeration<String>)。getHeaderNames(): 获取所有请求头的名称(返回Enumeration<String>)。getMethod(): 获取请求的 HTTP 方法(例如:“GET”, “POST”)。getRequestURI(): 获取请求的 URI。getContextPath(): 获取 Web 应用的上下文路径。getServletPath(): 获取 Servlet 的路径。getQueryString(): 获取查询字符串(URL 中?后面的部分)。getRemoteAddr(): 获取发送请求的客户端的 IP 地址。getRemoteHost(): 获取发送请求的客户端的主机名。getSession(): 返回当前请求关联的HttpSession,如果不存在则创建一个新的。getSession(boolean create): 返回当前请求关联的HttpSession,如果create为true且不存在则创建一个新的,否则返回null。setAttribute():HttpServletRequest的属性只在当前请求的处理过程中有效,当该请求的响应发送回客户端后,这些属性就不再需要了getAttribute():获取设置的属性。
练习1:获取get请求中的参数
name=ulna&age=20
package com.example;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import javax.servlet.ServletConfig;
import java.util.Enumeration;
import java.util.Map;
@WebServlet(name = "Test1", urlPatterns = {"/test1"})
public class HttpServletRequestTest1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 获取请求参数的最佳实践
//直接获取参数
String name = req.getParameter("name");
String age = req.getParameter("age"); //缺少参数获取值null
// 通过Map方法获取
Map <String, String[]> parameterMap = req.getParameterMap();
String nameMap = parameterMap.get("name")[0];
//String ageMap = parameterMap.get("age")[0]; //如果少参数会报错
String ageMap = null; //使用Map在获取值之前需要判断是否存在
if (parameterMap.containsKey("age")) {
ageMap = parameterMap.get("age")[0];
}
// 获取请求头的最佳实践
// 获取所有请求头名称
Enumeration<String> headerNames = req.getHeaderNames(); //获取所有请求头名称
// 只获取想要的请求头
String userAgent = req.getHeader("user-agent"); //获取User-Agent请求头
System.out.println("User-Agent: " + userAgent);
// 设置响应内容类型
resp.setContentType("text/html;charset=UTF-8");
// 输出响应内容
resp.getWriter().println("<html><body>");
resp.getWriter().println("<h1>Request Method: " + req.getMethod() + "</h1>");
resp.getWriter().println("<h1>Received Parameters</h1>");
resp.getWriter().println("<p>Name: " + name + "</p>");
resp.getWriter().println("<p>Age: " + age + "</p>");
resp.getWriter().println("<h2>Using Map Method</h2>");
resp.getWriter().println("<p>Name from Map: " + nameMap + "</p>");
resp.getWriter().println("<p>Age from Map: " + ageMap + "</p>");
resp.getWriter().println("<h2>Request Headers</h2>");
while (headerNames.hasMoreElements()) { //像迭代器一样访问请求头
String headerName = headerNames.nextElement();
//可以在这里对请求头进行处理
String headerValue = req.getHeader(headerName);
resp.getWriter().println("<p>" + headerName + ": " + headerValue + "</p>");
}
// 输出上下文路径
resp.getWriter().println("<h1>Context Path: " + req.getContextPath() + "</h1>"); //当前应用的上下文路径
// 输出Servlet路径
resp.getWriter().println("<h1>Servlet Path: " + req.getServletPath() + "</h1>"); //当前Servlet实例的路径
// 实际访问的路径 = 上下文路径 + Servlet路径
resp.getWriter().println("</body></html>");
}
}
HttpServletResponse 接口
HttpServletResponse 对象代表了Servlet发送给客户端的Http响应。通过这个对象,我们可以设置响应的内容、状态码、响应头等信息。
常用方法:
getWriter(): 返回一个PrintWriter对象,你可以使用它向客户端发送文本数据。getOutputStream(): 返回一个ServletOutputStream对象,你可以使用它向客户端发送二进制数据(例如:图片、文件)。setContentType(String type): 设置响应的内容类型(例如:“text/html”, “application/json”)。同时也可以设置字符编码,例如"text/html;charset=UTF-8"。setStatus(int sc): 设置响应的状态码(例如:200, 404, 500)。sendError(int sc, String msg): 发送一个错误响应到客户端,并设置状态码和错误消息。sendRedirect(String location): 发送一个临时的重定向响应,告诉客户端浏览器去访问另一个 URL。setHeader(String name, String value): 设置指定的响应头。addHeader(String name, String value): 添加指定的响应头(允许有多个同名 header)。setDateHeader(String name, long date): 设置日期格式的响应头。setIntHeader(String name, int value): 设置整数格式的响应头。setContentLength(int len): 设置响应内容的长度。
练习2:创建一个Servlet,当通过访问特定的URL时如:
/redirect-me, 他会使用sendRedirect() 方法将客户端重定向到另一个你指定的URL例如:https://www.google.com
@WebServlet(name = "Redirect", urlPatterns = {"/redirect-me"})
public class HttpServletRequestTest1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.sendRedirect("https://www.google.com");
}
}
ServletConfig 接口
每个Servlet都有一个对应的 ServletConfig 对象,Servlet 容器在构建Servlet时会创建这个对象,并传递给Servlet的 init(ServletConfig config)方法。ServletConfig 接口允许你访问Servlet的配置信息。
注册初始参数:
只有一个参数时:通常可以这样简化写
@WebServlet(name = "ConfigTest", urlPatterns = {"/configTest"})
@WebInitParam(name = "param1", value = "value1")
public class ConfigTest extends HttpServlet
多个参数时:规范写法
@WebServlet(name = "ConfigTest",
urlPatterns = {"/configTest"},
initParams = {
@WebInitParam(name = "param1", value = "value1"),
@WebInitParam(name = "param2", value = "value2"),
})
public class ConfigTest extends HttpServlet
常用方法:
getServletName(): 返回 Servlet 的名称,通常是在web.xml或使用@WebServlet注解时指定的。getInitParameter(String name): 返回 Servlet 初始化参数中指定名称的值(返回String)。这些参数可以在web.xml中通过<init-param>标签配置,或者在使用注解时通过@WebInitParam配置。getInitParameterNames(): 返回包含所有 Servlet 初始化参数名称的Enumeration<String>.getServletContext(): 返回当前 Web 应用的ServletContext对象。
练习3:创建一个Servlet,通过
@WebInitPara注解配置至少两个初始化参数。在Servlet的doGet()方法中获取并显示这些初始化参数的值以及servlet的名称。
@WebServlet(name = "ConfigTest",
urlPatterns = {"/configTest"},
initParams = {
@WebInitParam(name = "param1", value = "value1"),
@WebInitParam(name = "param2", value = "value2"),
})
public class ConfigTest extends HttpServlet {
private String param1;
private String param2;
@Override
public void init(ServletConfig config) throws ServletException{
super.init(config);
param1 = config.getInitParameter("param1");
param2 = config.getInitParameter("param2");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
resp.getWriter().println("Servlet Name: " + getServletConfig().getServletName()); //servlet中任何地方可以使用getServletConfig()获取该Servlet的Config
resp.getWriter().println("Param1: " + param1);
resp.getWriter().println("Param2: " + param2);
}
}
ServletContext 接口
ServletContext 接口代表了当前的Web应用程序。它是整个Web应用共享的上下文,所有的Servlet、Filter、Listener 等都可以访问同一个 ServletContext对象。Servlet 容器在启动Web应用时创建 ServletContext对象。
常用方法:
getAttribute(String name): 返回指定名称的应用程序级别的属性值(返回Object)。setAttribute(String name, Object object): 设置应用程序级别的属性/对象。removeAttribute(String name): 移除指定名称的应用程序级别的属性。getInitParameter(String name): 返回 Web 应用的上下文初始化参数中指定名称的值(返回String)。这些参数在web.xml的<context-param>标签中配置。getInitParameterNames(): 返回包含所有 Web 应用上下文初始化参数名称的Enumeration<String>.getResource(String path): 返回指定路径的资源 URL。路径以/开头,相对于 Web 应用的根目录。getResourceAsStream(String path): 返回指定路径的资源输入流。getContextPath(): 返回 Web 应用的上下文路径。getServletContextName(): 返回 Web 应用的名称(如果在部署描述符中指定了)。log(String msg): 将消息写入 Servlet 容器的日志。
练习4:
- 在你的web应用的web.xml文件种配置至少一个
<context-param>- 创建一个Servlet,该Servlet 获取并显示这个上下文参数的值。
- 在该Servlet中,设置一个ServletContext属性,并在同一个Servlet中获取并显示这个属性的值。
web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<context-param>
<param-name>appName</param-name>
<param-value>LearnServlet</param-value>
</context-param>
<context-param>
<param-name>blogName</param-name>
<param-value>https://blog.ulna520.top</param-value>
</context-param>
</web-app>
@WebServlet(name = "ContextTest",
urlPatterns = {"/contextTest"}
)
public class ContextTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
ServletContext context = getServletContext();
String appName = context.getInitParameter("appName");
String blogName = context.getInitParameter("blogName");
String contextPath = context.getContextPath();
String ServletContextName = context.getServletContextName(); //未设置得到null
Map<String,Integer> map = new HashMap<>();
map.put("appName", 1);
map.put("blogName", 2);
map.put("contextPath", 3);
map.put("ServletContextName", 4);
context.setAttribute("ServletContext", map); //可以设置Object对象,以供全局访问一个对象
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().println("<html><body>");
resp.getWriter().println("<h1>Context Parameters</h1>");
resp.getWriter().println("<p>Application Name: " + appName + "</p>");
resp.getWriter().println("<p>Blog Name: " + blogName + "</p>");
resp.getWriter().println("<p>Context Path: " + contextPath + "</p>");
resp.getWriter().println("<p>Servlet Context Name: " + ServletContextName + "</p>");
resp.getWriter().println("<h2>Context Attributes</h2>");
resp.getWriter().println("<ul>");
Map<String, Integer> contextMap = (Map<String, Integer>) context.getAttribute("ServletContext");
for (Map.Entry<String, Integer> entry : contextMap.entrySet()) {
resp.getWriter().println("<li>" + entry.getKey() + ": " + entry.getValue() + "</li>");
}
resp.getWriter().println("</ul>");
resp.getWriter().println("</body></html>");
}
}
Servlet 请求转发与重定向
请求转发
请求转发是指将当前请求的处理交给web服务器上的另一个资源来完成,这个过程发生在服务器内部,客户端浏览器只发送一次请求(与重定向的一个关键不同)。
实现方式:
通过 RequestDispatcher 接口来实现请求转发。我们可以通过 HttpServletRequest 对象的 getRequestDispatcher(String path) 方法获取一个 RequestDispatcher对象。path 可以是相对于当前Servlet上下文路径。
RequestDispatcher 接口提供了两个主要方法用于转发:
forword(ServletRequest request, ServletResponse response):将请求转发给另一个资源。- 注意:在转发后,
forword()后的代码有可能会执行。但是在调用之后,当前Servlet不应该再向客户端发送任何响应。最佳实践是forwrod()后就立即结束当前方法。
- 注意:在转发后,
include(ServletRequest request, ServletResponse response):将另一个资源的内容包含到当前响应中。执行流程会回到当前Servlet继续处理。
@WebServlet("/first")
public class FirstServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("message", "Hello from FirstServlet");
RequestDispatcher dispatcher = request.getRequestDispatcher("/second");
System.out.println("FirstServlet: Forwarding request to /second");
dispatcher.forward(request, response);
System.out.println("FirstServlet: After forwarding"); // 这行代码可能不会立即执行,取决于 SecondServlet 的处理
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
@WebServlet("/second")
public class SecondServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String message = (String) request.getAttribute("message");
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html><head><title>Second Servlet</title></head><body>");
out.println("<h1>Second Servlet 处理请求</h1>");
if (message != null) {
out.println("<p>Message from previous servlet: " + message + "</p>");
} else {
out.println("<p>No message passed.</p>");
}
out.println("</body></html>");
System.out.println("SecondServlet: Request processed.");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
请求重定向
请求重定向是指服务器告诉客户端浏览器发送一个新的HTTP请求到另一个URL。这涉及到两次HTTP请求-响应周期。
实现方式:
通过 HttpServletResponse 对象的 sendRedirect(String location)方法来发送重定向响应。
location 是客户端浏览器需要重新访问的URL。
@WebServlet("/redirect-google")
public class RedirectServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("RedirectServlet: Received request, redirecting to Google.");
response.sendRedirect("https://www.google.com");
System.out.println("RedirectServlet: After sending redirect.");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
Filter
Filter基础知识
Filter(过滤器)是Servlet规范中一个非常重要的组件,它的核心作用是在请求到达目标Servlet或JSP之前,以及响应离开目标Servlet/JSP后,对请求和响应进行截获和处理。
Fliter的作用:
在没有Filter之前,如果我们需要对所有请求进行一些通用操作(比如权限检查、编码设置),我们就在每个Servlet中重复编写这些逻辑。这回导致大量的重复代码,难以维护。Filter的出现就是为了解决这个问题,它允许我们将这些通用的、与具体业务逻辑无关的功能从Servlet中分离出来,集中处理。
应用场景:
- 权限认证:检查用户是否已经登录,是否有权访问某个资源。
- 日志记录:记录每个请求的详细信息,例如请求路径、用户IP、请求时间、响应耗时等。
- 字符编码处理:统一设置请求和响应的字符编码,防止中文乱码问题。
- 数据压缩:对响应内容进行GZIP压缩,减少网络传输量,提高加载速度。
- 敏感词过滤:对用户提交的数据进行过滤,例如评论中的敏感词。
- 缓存控制:设置HTTP响应头,控制客户端的缓存行为。
- 会话管理:检查会话是否存在,或在会话过期时重定向。
Filter的生命周期
Filter也有自己的生命周期,由Servlet容器如(Tomcat)管理:
-
加载和实例化:当Web应用程序启动或者容器首次需要使用某个Filter时,容器会加载并创建Filter的实例。
-
初始化(
init()):容器会调用Filter实例的init()方法。这个方法只被调用一次,用于执行一些初始化操作,比如读取配置参数、简历数据库连接等。方法签名:
void init(FilterConfig filterConfig) throws ServletException -
请求处理(
doFilter()):当客户端请匹配到该Filter拦截的URL模式时,容器就会调用Filter实例的doFilter()方法。这是Filter执行核心逻辑的地方。 方法签名:void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException -
销毁(
destroy()):当Web应用程序关闭或者Filter被卸载时,容器会调用Filter实例的destroy()方法。这个方法也只会调用一次,用于释放资源,比如关闭数据库连接、清理缓存等。方法签名:
void destroy()
Filter三大接口
javax.servlet.Filter接口
javax.servlet.Filter 是所有自定义过滤器的基石。它是一个接口,定义了Filter必须实现的三个核心方法。
- init()
- doFilter()
- destroy()
package javax.servlet;
import java.io.IOException;
public interface Filter {
/**
* 由 Web 容器在 Filter 实例化后立即调用,只调用一次。
* 用于执行初始化任务。
* @param filterConfig Filter 的配置对象
* @throws ServletException 如果初始化过程中发生错误
*/
void init(FilterConfig filterConfig) throws ServletException;
/**
* 这是 Filter 的核心方法,每当请求匹配到 Filter 映射的 URL 模式时都会调用。
* 在这里可以对请求和响应进行预处理或后处理。
* @param request Servlet 请求对象
* @param response Servlet 响应对象
* @param chain Filter 链,用于将请求传递给下一个 Filter 或目标资源
* @throws IOException 如果发生 I/O 错误
* @throws ServletException 如果发生 Servlet 错误
*/
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
/**
* 由 Web 容器在 Filter 实例被销毁前调用,只调用一次。
* 用于释放 Filter 所持有的资源。
*/
void destroy();
}
javax.servlet.FilterChain 接口
FilterChain(过滤器链)是Filter机制中一个至关重要的概念。当一个请求进入一个Web应用程序时,如果它需要经过多个Filter的处理,这些Filter就会被组织成一个链条。FilterChain 就是这个链条的抽象。
FilterChain 接口只有一个核心方法:
package javax.servlet;
import java.io.IOException;
public interface FilterChain {
/**
* 将请求和响应传递给链中的下一个过滤器。
* 如果这是链中的最后一个过滤器,则将请求传递给目标资源 (Servlet 或 JSP)。
* @param request Servlet 请求对象
* @param response Servlet 响应对象
* @throws IOException 如果发生 I/O 错误
* @throws ServletException 如果发生 Servlet 错误
*/
void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException;
}
在自定义Filter的doFilter()方法中,通常会使用到 chain.doFilter(request,response)
作用:
- 如果当前Filter不是链中的最后一个Filter,它会将请求和响应对象传递给下一个Filter。
- 如果当前Filter是链中的最后一个Filter,它会将请求和响应对象传递给最终的目标资源(例如Servlet或JSP)
如果没用调用 chain.doFilter(request, response),那么请求就不会继续传递下去,它将被中断,后续的Filter或目标资源将永远不会执行到。
javax.servlet.FilterConfig 接口
FilterConfig 对象提供了Filter的配置信息。当容器调用Filter的 init()方法时,会将一个 FilterConfig实例作为参数传递进来。
FilterConfig接口提供的方法包括:
String getFilterName():获取Filter的名称(在web.xml中<filter-name>定义的名称,或者@WebFilter注解的filterName属性)ServletContext getServletContext():获取与此Filter相关的ServletContext对象。String getInitparameter(String name):根据参数名获取Filter的初始化参数值。(在配置文件中设置的参数)Enumeration<String> getInitParameterNames():获取所有初始化参数的名称。
创建与配置Filter
创建Filter
创建Filter主要涉及两步:
- 实现
javax.servlet.Filter接口 - 重写其核心方法。
步骤一:创建一个Java类并实现Filter接口
我们需要创建一个普通的Java类,并让它实现 javax.servlet.Filter接口。
package com.example.filters;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
public class MyCustomFilter implements Filter{
private String filterName; // 存储 Filter 的名称
/**
* Filter 初始化方法,在 Filter 实例创建后被 Web 容器调用一次。
* 可以在这里读取初始化参数,进行一些资源准备。
* @param filterConfig 包含 Filter 配置信息的对象
* @throws ServletException 如果初始化失败
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.filterName = filterConfig.getFilterName();
System.out.println(filterName + " 初始化...");
// 示例:获取 Filter 的初始化参数
String paramValue = filterConfig.getInitParameter("myParam");
if (paramValue != null) {
System.out.println("获取到初始化参数 'myParam': " + paramValue);
}
}
/**
* 核心方法,每次请求匹配到该 Filter 时都会被调用。
* 在这里实现你的过滤逻辑。
* @param request ServletRequest 对象
* @param response ServletResponse 对象
* @param chain FilterChain 对象,用于将请求传递 给下一个 Filter 或目标资源
* @throws IOException 如果发生 I/O 错误
* @throws ServletException 如果发生 Servlet 错误
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println(filterName + " 正在执行 doFilter() (请求进入前)");
// 继续将请求传递给 Filter 链中的下一个 Filter 或目标资源
chain.doFilter(request, response);
// 后处理,在请求处理完毕、响应返回前被执行
System.out.println(filterName + " 正在执行 doFilter() (请求离开后)");
}
/**
* Filter 销毁方法,在 Filter 实例被销毁前被 Web 容器调用一次。
* 可以在这里释放资源。
*/
@Override
public void destroy() {
System.out.println(filterName + " 正在销毁...");
// 示例:关闭数据库连接、清理资源等
}
}
配置Filter
在 web.xml中配置Filter(传统方式)
这是Servlet 2.X 和早期版本的主要配置方式,在Servlet 3.0+中依然完全有效,并且在需要精准控制Filter顺序时非常有效。
-
打开或创建
web.xml文件 通常位于你的Web项目的WEB-INF/web.xml路径下。 -
声明Filter 使用
<filter>标签在web.xml中声明你的Filter。<filter-name>:为你的Filter定义一个唯一的名称。<filter-class>:指定你的Filter类的完全限定名。<init-param>(可选):为Filter提供初始化参数,这些参数可以在init()方法中通过FilterConfig获取。
<filter> <filter-name>MyFilter</filter-name> <filter-class>com.example.filters.MyCustomFilter</filter-class> <init-param> <param-name>myParam</param-name> <param-value>HelloFilter</param-value> </init-param> <init-param> <param-name>anotherParam</param-name> <param-value>123</param-value> </init-param> </filter> -
映射Filter到URL模式:
使用
<filter-mapping>标签将声明的Filter映射到特定的URL模式。只有匹配这些模式的请求才会经过此Filter。<filter-name>:<url-pattern>:/*:拦截所有的请求/user/*:拦截所有以/user/开头的请求*.jsp:拦截所有.jsp结尾的请求/specific/path:拦截精确匹配的路径
<dispatcher>(可选):指定Filter应该在哪种请求调度类型下被使用。常见的有:REQUEST(默认):当客户端直接请求资源时。FORWARD:当通过RequestDispatcher.forward()转发时。INCLUDE:当通过RequestDispatcher.include()包含时。ERROR:当请求因为错误而转发到错误页面时。ASYNC:当请求通过异步上下文处理时(Servlet3.0+)
<filter-mapping> <filter-name>MyFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> </filter-mapping>在
web.xml中,Filter的执行顺序是根据它们在<web-app>标签内<filter-mapping>元素的出现顺序来决定的。容器会按照web.xml文件中<filter-mapping>声明的从上到下的顺序来构建Filter链并依次执行。如果是url不匹配的Filter,则跳过。一直执行到最终资源。
使用注解 @WebFilter配置Filter(Servlet3.0+)
从Servlet3.0 开始,你可以使用注解来简化Filter的配置,省去了编写web.xml的麻烦。
在Filter类上添加 @WebFilter注解
filterName(可选):指定Filter的名称,默认为类名。urlPatterns:一个字符串数组(可以拦截多个路径),用于指定Filter将拦截的URL模式value:urlPatterns的别名。如果只有一个URL模式,可以直接用value。initParams(可选):一个@WebInitParam数组,用于设置初始化参数。dispatcherTypes(可选):一个DispatcherType数组,指定Filter应该在哪种请求调度类型下被调用。
package com.example.filters;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.DispatcherType; // 导入 DispatcherType
import java.io.IOException;
// 使用注解配置 Filter
@WebFilter(
filterName = "AnnotatedMyFilter", // Filter 的名称
urlPatterns = {"/*"}, // 拦截所有请求
initParams = { // 初始化参数
@WebInitParam(name = "myParam", value = "HelloAnnotatedFilter")
},
dispatcherTypes = {DispatcherType.REQUEST, DispatcherType.FORWARD} // 调度类型
)
public class MyCustomFilter implements Filter {
// ... (与上面 web.xml 方式的 Filter 类内容相同) ...
private String filterName;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.filterName = filterConfig.getFilterName();
System.out.println(filterName + " 初始化...");
String paramValue = filterConfig.getInitParameter("myParam");
if (paramValue != null) {
System.out.println("获取到初始化参数 'myParam': " + paramValue);
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println(filterName + " 正在执行 doFilter() (请求进入前)");
chain.doFilter(request, response);
System.out.println(filterName + " 正在执行 doFilter() (请求离开后)");
}
@Override
public void destroy() {
System.out.println(filterName + " 正在销毁...");
}
}
Filter顺序:如前所述,在使用
@WebFilter时,默认情况下Filter的执行顺序是不保证的(通常按照filterName的字母顺序或者类加载顺序),如果你需要精确的控制顺序,建议使用web.xml配置,或者在Spring等框架中使用@Order注解
Listener
在Java Servlet中,Listener(监听器)是一种特殊的组件,它的主要作用是”监听”Web应用程序中发生的特定事件。当某个预设事件发生时,Listener就会被触发,然后执行你为它定义的特定操作。
这些事件可以是:
- Web应用程序自身的生命周期事件
- 用户会话(Session)的生命周期事件
- HTTP请求的生命周期事件
- 特定作用域(应用程序、会话、请求)中属性的添加、移除或替换事件。
Listener的核心作用:
Listener的存在,让开发者能够在这些关键时刻介入并执行自定义的逻辑,而无需修改Servlet或JSP页面的核心业务代码。它的主要作用体现在以下几个方面:
- 资源初始化与清理:
- 作用:在Web应用程序启动时,你可以使用Listener来加载配置文件、初始化数据库连接池、启动后台服务等一次性设置。而在应用程序关闭时,它可以用来关闭连接、释放资源、确保系统干净的退出。
- 职责分离:Servlet或Filter的init()方法只在特定Servlet被初始化时,这些资源才会被初始化。如果我有多个Servlet都需要这些资源,那么每个Servlet都需要重复初始化逻辑,Filter同理。Listener专注于应用程序级别的生命周期管理和全局资源的初始化/清理。它的代码更清晰,专门处理“当应用程序启动/关闭时,我需要做什么”的问题。
- 数据统计与监控:
- 作用:Listener可以用来实时跟踪网站的使用情况。例如,你可以计算当前在线用户数量,记录每天的访问量,或者监控某个特定功能的使用频率。
- 例子:你想知道有多少用户同时在线。你可以使用Listener监听会话的创建和销毁事件,每次创建就+1,销毁就-1,从而得到在线用户数量。
- 日志记录:
- 作用:在应用程序、会话或请求的关键生命周期点记录日志,帮助开发者追踪应用程序的行为、诊断问题或进行性能分析。
- 例子:每当一个用户请求进入或离开服务时,你想记录下请求的URL和处理时间。Listener就可以做到这一点。
Litener接口
Java Servlet 规范提供了一系列Listener接口,它们被设计用来监听不同范围和类型的事件。掌握这些接口是使用Listener的基础。
应用(Web容器)生命周期Listener
这类Listener用于监听整个Web应用程序的启动和关闭事件。
ServletContextListener:
- 作用:监听Web应用程序(ServletContext)的生命周期事件。当整个Web应用程序被部署到Servlet容器(如Tomcat)并启动时,以及当应用程序被关闭或卸载时,它就会收到通知。
- 接口方法:
void contextInitialized(ServletContextEvent sce):当Web应用程序启动时,此方法会被调用。我们可以在这里执行全局性初始化工作。例如:- 初始化数据库连接池
- 加载应用程序的全局配置参数
- 启动后台服务或定时任务
- 初始化日志系统
void contextDestroyed(ServletContextEvent sce):当Web应用程序关闭或被卸载时,此方法会被调用。我们可以在这里执行资源清理工作,例如:- 关闭数据库连接池
- 释放占用的内存或文件句柄
- 停止后台服务
- 使用场景:这是最常用的Listener之一,适用于那些需要在应用程序级别进行一次性设置和清理的任务。
会话(Session)生命周期Listener
这类Listener用于监听用户会话的创建和销毁事件。
HttpSessionListener:
- 作用:监听HTTP会话(HttpSession)的生命周期事件。当用户第一次访问网站并创建会话时,以及当会话过期或被明确销毁时,它就收到通知。
- 接口方法:
void sessionCreated(HttpSessionEvent se):当新的Http会话被创建时调用此方法。我们可以用这个事件:- 统计在线用户数量
- 为新会话设置默认属性
void sessionDestroyed(HttpSessionEvent se):当HTTP会话失效(过期或被显示调用invalidate())时调用此方法。我们可以用这个事件来:- 统计在线用户数
- 清理与该会话相关的资源
请求生命周期Listener
这类Listener用于监听每个HTTP请求的开始和结束事件。
ServletRequestListener:
- 作用:监听HTTP请求(ServletRequest)的生命周期事件。在每个HTTP请求被Web容器接收并开始处理时,以及在请求处理完毕即将发送响应回客户端时,它都会收到通知。
- 接口方法:
void requestInitialized(ServletRequestEvent sre):当每个HTTP请求开始被处理时调用此方法。我们可以在这里:- 记录请求的开始时间,用于性能分析。
- 初始化与当前请求相关的特定资源。
void requestDestroyed(ServletRequestEvent sre):当每个HTTP请求处理完成并即将响应给客户端时调用此方法。我们可以在这里:- 记录请求的结束时间并计算处理耗时
- 清理与当前请求相关的临时资源
- 使用场景:常用于请求级别的性能监控,日志记录等。
属性变更Listener
这类Listener用于监听特定作用域(ServletContext、HttpSession、ServletRequest)中属性的添加、移除、替换事件。当调用setAttribute()、removeAttribute()或replaceAttribute()方法时,这些Listener就会被触发。
ServletContextAttributeListener:
- 作用:监听ServletContext(应用程序作用域)中属性的变化。
- 接口方法:
attributeAdded(ServletContextAttributeEvent scae)attributeRemoved(ServletContextAttributeEvent scae)attributeReplaced(ServletContextAttributeEvent scae)
HttpSessionAttributeListener:
- 作用:监听HttpSession(会话作用域)中属性的变化
- 接口方法:
attributeAdded(HttpSessionBindingEvent hsbe)attributeRemoved(HttpSessionBindingEvent hsbe)attributeReplaced(HttpSessionBindingEvent hsbe)
ServletRequestAttributeListener:
- 作用:监听ServletRequest(请求作用域)中属性的变化
- 接口方法:
attributeAdded(ServletRequestAttributeEvent srae)attributeRemoved(ServletRequestAttributeEvent srae)attributeReplaced(ServletRequestAttributeEvent srae)
使用场景:用于监控和响应特定作用域内数据的动态变化,例如:
- 当某个关键配置属性在应用程序级别被修改时,触发重新加载
- 跟踪会话中用户登录状态的变化
- 请求处理的过程中,根据属性变化执行特定逻辑
会话绑定Listener
这类Listener专注于监听Java对象何时被绑定到 HttpSession或从 HttpSession中解除绑定。
HttpSessionBindingListener:
- 作用:这是一个特殊的Listener,它不是由容器注册的。而是被绑定到Session的Java对象本身实现。当一个实现了此接口的对象被HttpSession.setAttribute()存储到Session中时,其valueBound()方法会被调用;当它被HttpSession.removeAttribute()或Session失效时,其valueUnbound()方法会被调用。
- 接口方法:
void valueBound(HttpSessionBindingEvent event)void valueUnbound(HttpSessionBindingEvent event)
- 使用场景:当某个对象需要知道它何时被放入或移除Session时非常有用,例如:
- 一个用户对象,当它被绑定到Session时,可以更新数据库中的在线状态。
- 一个资源句柄,当它被从Session移除时,可以自动释放资源。
Listener与原来执行代码的执行顺序
当Listener被触发时,它与原来的代码之间,通常是顺序执行的关系,而不是并行执行。
- 请求生命周期:请求开始(调用Listener)->进入Filter链->进入Servlet->离开Servlet->离开Filter->请求结束(调用Listener)
- 属性变更:
setAttribute()等方法本身会先执行,完成属性的设置操作,然后相关的Listener才会被触发。
创建与配置Listener类
创建Listener类
步骤:
- 创建一个普通的Java类
- 让这个类实现你想要监听的事件对应的Listener接口
- 重写该接口中定义的所有方法,并在这些方法中编写你的业务逻辑
示例:
package com.example.listeners; // 建议放在一个专门的包中
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener; // 如果你想使用注解方式配置
// 使用注解配置方式时需要添加此注解
// @WebListener
public class MyApplicationListener implements ServletContextListener {
/**
* 当Web应用程序启动时调用此方法。
* @param sce ServletContextEvent 事件对象,可以获取ServletContext
*/
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("🎉🎉🎉 Web应用程序启动啦!在 " + sce.getServletContext().getContextPath() + ".");
// 在这里可以执行数据库连接池初始化、加载配置文件等全局性设置
// ServletContext context = sce.getServletContext();
// context.setAttribute("myGlobalData", "Some initialized data");
}
/**
* 当Web应用程序关闭时调用此方法。
* @param sce ServletContextEvent 事件对象
*/
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("👋👋👋 Web应用程序关闭了。清理资源...");
// 在这里可以执行关闭数据库连接、释放内存等资源清理工作
}
}
配置Listener
创建好Listener类后,你需要告诉Servlet容器你的这个类是一个Listener,并且希望它在特定的事件发生时被调用。
方法一:使用 web.xml文件配置(传统方式)
这是在Servlet3.0之前以及需要最大兼容性时使用的标准方式。
- 在
<web-app>标签的内部添加<listener>标签 <listener>标签内部,使用<listener-class>标签指定Listener类的完全限定名(包括包名)
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>MyWebAppWithListeners</display-name>
<listener>
<listener-class>com.example.listeners.MyApplicationListener</listener-class>
</listener>
</web-app>
方法二:使用注解(Annotations)配置
从Servlet3.0开始,你可以使用 @WebListener注解来简化Listener的配置,无需修改web.xml。这是目前主流更简洁的方式。
package com.example.listeners;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener; // 导入WebListener注解
@WebListener // 标记这个类为一个Web Listener
public class MyApplicationListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("🎉🎉🎉 Web应用程序启动啦!(通过注解配置) 在 " + sce.getServletContext().getContextPath() + ".");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("👋👋👋 Web应用程序关闭了。(通过注解配置) 清理资源...");
}
}
简单实战
xxy1103/javaWeb: 一个简单的Servlet、Filter、Listener的演示示例。一个简单的登录验证系统,同时统计登录在线人数。