学习目标:
- 掌握 ServletContextListener 的使用,理解其执行时机和生命周期
- 掌握 Filter 的使用,理解其执行时机和过滤器链机制
- 能够使用 Filter 解决实际开发中的常见问题(字符编码、登录验证、日志记录等)
- 理解三大 Web 组件的协作关系
本章重点:
- Listener 与 Filter 在 Web 请求链路中的职责定位
- ServletContextListener 的初始化与资源清理场景
- Filter 的执行顺序、过滤器链机制与拦截/放行逻辑
- 字符编码、登录验证、日志记录、跨域处理等典型过滤器场景
1. 前置知识准备
- Servlet的执行流程和生命周期
- ServletContext的功能和使用
- HTTP请求/响应处理
2. Web组件概述
JavaEE定义了三大Web组件,它们共同构成了Web应用的核心处理机制:
| 组件 | 职责 | 执行时机 |
|---|---|---|
| Servlet | 处理请求对应的业务逻辑 | 接收到请求时 |
| Listener | 监听Web应用生命周期事件 | 应用启动/关闭时 |
| Filter | 对请求/响应进行过滤处理 | Servlet前后 |
三者的执行顺序:Listener → Filter → Servlet → Filter
3. Listener监听器
3.1 什么是监听器
监听器(Listener)是JavaEE提供的事件监听机制。监听器会监听特定的主体(如ServletContext、HttpSession、ServletRequest等),当主体发生特定事件(初始化、销毁、属性变化等)时,自动触发对应的方法。
💡 核心思想:通过监听机制,在关键生命周期节点自动执行初始化或清理操作。
3.2 ServletContextListener
ServletContextListener是最常用的监听器,用于监听ServletContext的生命周期:
| 事件 | 触发时机 | 对应方法 |
|---|---|---|
| 初始化 | 应用程序启动时 | contextInitialized() |
| 销毁 | 应用程序关闭时 | contextDestroyed() |
生命周期说明
应用程序启动
↓
ServletContext初始化
↓
触发contextInitialized() ← 在这里进行资源初始化
↓
应用运行中...
↓
应用程序关闭
↓
ServletContext销毁
↓
触发contextDestroyed() ← 在这里进行资源释放3.2.1 执行过程图解
当应用程序启动时,Web组件按以下顺序加载:
- 首先加载:ServletContext 和 Listener
- 然后加载:loadOnStartup ≥ 0 的Servlet
应用启动 ├── 创建ServletContext ├── 执行ServletContextListener.contextInitialized() │ └── 初始化全局资源(如数据库连接池、SqlSessionFactory) ├── 初始化loadOnStartup ≥ 0的Servlet └── 应用就绪,等待请求
3.3 配置方式
方式一:注解配置(推荐)
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener // 无需指定value,自动监听
public class CustomServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 应用启动时执行
System.out.println("✅ 应用程序启动,ServletContext初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 应用关闭时执行
System.out.println("❌ 应用程序关闭,ServletContext销毁");
}
}方式二:web.xml配置
<?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">
<!-- 监听器配置 -->
<listener>
<listener-class>com.example.listener.CustomServletContextListener</listener-class>
</listener>
</web-app>⚠️ 勘误说明:两种方式等效,但注解配置更简洁,是Servlet 3.0+的推荐方式。
<!-- [PRACTICE-START] 实战练习内容已提取到 09-web-integration-practice.md -->
3.4 实战案例:MyBatis集成优化
场景描述
之前整合MyBatis时,SqlSessionFactory在Servlet的init()方法中初始化。更好的做法是在应用启动时就完成初始化,并存入ServletContext供全局使用。
完整代码实现
监听器代码:
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.io.IOException;
import java.io.InputStream;
@WebListener
public class MyBatisInitListener implements ServletContextListener {
private static final String CONFIG_FILE = "mybatis-config.xml";
private static final String CONTEXT_KEY = "SqlSessionFactory";
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
try (InputStream is = Resources.getResourceAsStream(CONFIG_FILE)) {
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
// 存入ServletContext,全局共享
context.setAttribute(CONTEXT_KEY, factory);
System.out.println("✅ MyBatis初始化成功,SqlSessionFactory已存入ServletContext");
} catch (IOException e) {
System.err.println("❌ MyBatis初始化失败: " + e.getMessage());
throw new RuntimeException("MyBatis初始化失败", e);
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 清理资源
ServletContext context = sce.getServletContext();
context.removeAttribute(CONTEXT_KEY);
System.out.println("✅ MyBatis资源已清理");
}
}Servlet中使用:
import org.apache.ibatis.session.SqlSessionFactory;
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;
@WebServlet("/user/list")
public class UserListServlet extends HttpServlet {
private SqlSessionFactory sqlSessionFactory;
@Override
public void init() throws ServletException {
// 从ServletContext获取已初始化的工厂
sqlSessionFactory = (SqlSessionFactory) getServletContext()
.getAttribute("SqlSessionFactory");
if (sqlSessionFactory == null) {
throw new ServletException("SqlSessionFactory未初始化");
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 使用sqlSessionFactory执行业务操作
// ...
}
}<!-- [PRACTICE-END] 练习一:MyBatis集成优化 结束 -->
3.5 其他常用监听器
| 监听器接口 | 监听对象 | 用途 |
|---|---|---|
HttpSessionListener |
Session | 统计在线人数、Session超时处理 |
ServletRequestListener |
Request | 请求日志记录、性能监控 |
HttpSessionAttributeListener |
Session属性 | 监听登录/登出状态变化 |
4. Filter过滤器
4.1 什么是过滤器
Filter(过滤器)是运行在Servlet之前的组件,可以对请求进行预处理,对响应进行后处理。
执行位置
客户端请求
↓
Filter预处理(请求过滤)
↓
Servlet业务处理
↓
Filter后处理(响应过滤)
↓
返回客户端4.2 Filter和Servlet的映射关系
| 对比项 | Servlet | Filter |
|---|---|---|
| URL映射 | 1个URL → 1个Servlet | 1个URL → 多个Filter |
| 执行顺序 | 按URL匹配 | 按配置顺序 |
| 终止性 | 处理请求 | 可选择放行或拦截 |
📌 重要:一个请求可以经过多个Filter,形成过滤器链(FilterChain)。
4.3 过滤器链执行流程
单个Filter执行流程
请求 → Filter.doFilter()前半部分
↓
chain.doFilter() ← 放行
↓
Servlet处理
↓
Filter.doFilter()后半部分 → 响应多个Filter执行流程
假设配置了FilterA、FilterB、FilterC三个过滤器:
请求 → FilterA → FilterB → FilterC → Servlet
↓
响应 ← FilterA ← FilterB ← FilterC ← 处理完成执行顺序遵循先进后出(类似栈结构):
FilterA开始
FilterB开始
FilterC开始
Servlet处理
FilterC结束
FilterB结束
FilterA结束关键方法说明
// 放行方法:必须调用才能继续后续流程
filterChain.doFilter(request, response);⚠️ 勘误说明:如果不调用
chain.doFilter(),请求将被拦截,不会到达Servlet。
4.4 Filter生命周期
| 阶段 | 方法 | 执行次数 | 用途 |
|---|---|---|---|
| 初始化 | init(FilterConfig) |
1次 | 读取配置参数 |
| 过滤 | doFilter() |
每次请求 | 执行过滤逻辑 |
| 销毁 | destroy() |
1次 | 释放资源 |
4.5 配置方式
方式一:注解配置(推荐)
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 java.io.IOException;
@WebFilter("/*") // 过滤所有请求
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化操作
String encoding = filterConfig.getInitParameter("encoding");
System.out.println("Filter初始化,编码参数: " + encoding);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 前置处理
System.out.println("请求到达Filter");
// 放行
chain.doFilter(request, response);
// 后置处理
System.out.println("响应离开Filter");
}
@Override
public void destroy() {
// 销毁操作
System.out.println("Filter销毁");
}
}常用URL模式:
| 模式 | 含义 | 示例 |
|---|---|---|
/* |
所有请求 | 全局字符编码 |
/user/* |
以/user/开头的请求 | 登录验证 |
/admin/* |
以/admin/开头的请求 | 权限控制 |
*.jsp |
所有JSP页面 | 页面权限 |
方式二:web.xml配置
<web-app>
<!-- Filter定义 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>com.example.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<!-- Filter映射 -->
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>⚠️ 勘误说明:多个Filter时,web.xml中的配置顺序决定了执行顺序;注解配置时,Filter名称的字典序影响顺序(建议使用web.xml精确控制)。
<!-- [PRACTICE-START] 实战练习内容已提取到 09-web-integration-practice.md -->
4.6 实战案例
案例一:字符编码过滤器(最常用)
问题描述:POST请求中文乱码、响应中文乱码。
解决方案:
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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 字符编码过滤器
* 统一设置请求和响应的字符编码为UTF-8
*/
@WebFilter("/*")
public class CharacterEncodingFilter implements Filter {
private String encoding = "UTF-8";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 读取初始化参数
String configEncoding = filterConfig.getInitParameter("encoding");
if (configEncoding != null && !configEncoding.isEmpty()) {
this.encoding = configEncoding;
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 设置请求编码(解决POST中文乱码)
httpRequest.setCharacterEncoding(encoding);
// 设置响应编码(解决响应中文乱码)
httpResponse.setContentType("application/json;charset=" + encoding);
// 或者 HTML: text/html;charset=UTF-8
// 放行
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 清理资源
}
}案例二:登录验证过滤器
需求:访问/user/*、/order/*等路径时,必须已登录。
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* 登录验证过滤器
* 拦截需要登录才能访问的资源
*/
@WebFilter({"/user/*", "/order/*", "/cart/*"})
public class LoginCheckFilter implements Filter {
// 白名单:不需要登录就能访问的路径
private static final List<String> WHITE_LIST = Arrays.asList(
"/user/login",
"/user/register",
"/user/logout"
);
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestURI = httpRequest.getRequestURI();
String contextPath = httpRequest.getContextPath();
String path = requestURI.substring(contextPath.length());
// 1. 检查是否在白名单中
if (isWhiteListed(path)) {
chain.doFilter(request, response);
return;
}
// 2. 检查是否已登录
HttpSession session = httpRequest.getSession(false);
Object user = (session != null) ? session.getAttribute("user") : null;
if (user == null) {
// 未登录,返回401或重定向到登录页
// AJAX请求
if (isAjaxRequest(httpRequest)) {
httpResponse.setStatus(401);
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.getWriter().write("{\"code\":401,\"msg\":\"请先登录\"}");
} else {
// 普通请求,重定向到登录页
httpResponse.sendRedirect(contextPath + "/login.html");
}
return; // 不放行,中断请求
}
// 已登录,放行
chain.doFilter(request, response);
}
/**
* 检查路径是否在白名单中
*/
private boolean isWhiteListed(String path) {
return WHITE_LIST.stream().anyMatch(path::startsWith);
}
/**
* 判断是否为AJAX请求
*/
private boolean isAjaxRequest(HttpServletRequest request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}案例三:请求日志记录过滤器
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 请求日志过滤器
* 记录每个请求的访问信息
*/
@WebFilter("/*")
public class RequestLoggingFilter implements Filter {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 记录请求开始时间
long startTime = System.currentTimeMillis();
// 获取请求信息
String clientIP = getClientIP(httpRequest);
String method = httpRequest.getMethod();
String uri = httpRequest.getRequestURI();
String userAgent = httpRequest.getHeader("User-Agent");
System.out.println("┌──────────────────────────────────────────");
System.out.println("│ 【请求时间】" + LocalDateTime.now().format(FORMATTER));
System.out.println("│ 【客户端IP】" + clientIP);
System.out.println("│ 【请求方式】" + method);
System.out.println("│ 【请求路径】" + uri);
System.out.println("│ 【浏览器】" + (userAgent != null ? userAgent.substring(0,
Math.min(userAgent.length(), 50)) : "Unknown"));
try {
// 放行
chain.doFilter(request, response);
} finally {
// 记录响应时间
long duration = System.currentTimeMillis() - startTime;
System.out.println("│ 【处理耗时】" + duration + "ms");
System.out.println("└──────────────────────────────────────────");
}
}
/**
* 获取客户端真实IP(考虑反向代理)
*/
private String getClientIP(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
// 多个IP时取第一个
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}案例四:CORS跨域过滤器
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 跨域请求处理过滤器
* 解决前后端分离项目的跨域问题
*/
@WebFilter("/*")
public class CORSFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 允许的源(生产环境应配置具体域名)
httpResponse.setHeader("Access-Control-Allow-Origin", "*");
// 允许的请求方法
httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
// 允许的请求头
httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
// 预检请求缓存时间
httpResponse.setHeader("Access-Control-Max-Age", "3600");
// 允许携带Cookie
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
// 预检请求直接返回
if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
httpResponse.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}<!-- [PRACTICE-END] 练习二至练习五(Filter实战案例)结束 -->
5. 常见问题与解决方案
5.1 Filter相关问题
Q1: Filter不生效怎么办?
排查步骤:
- 检查URL模式:确认请求的URL匹配Filter的url-pattern
- 检查配置方式:注解和web.xml不要重复配置,可能冲突
- 检查是否放行:确认调用了
chain.doFilter() - 检查执行顺序:可能被前面的Filter拦截了
Q2: 多个Filter的执行顺序怎么控制?
解决方案:
| 配置方式 | 顺序控制方法 |
|---|---|
| web.xml | 按<filter-mapping>的配置顺序 |
| 注解 | 无法控制(按类名字典序),建议使用web.xml |
推荐在web.xml中集中配置Filter顺序:
<!-- 先执行字符编码 -->
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 再执行登录验证 -->
<filter-mapping>
<filter-name>LoginCheckFilter</filter-name>
<url-pattern>/user/*</url-pattern>
</filter-mapping>Q3: Filter中如何获取Spring容器中的Bean?
// 通过WebApplicationContext获取
WebApplicationContext context = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
UserService userService = context.getBean(UserService.class);5.2 Listener相关问题
Q1: contextInitialized执行两次?
原因:Tomcat配置了多个Context,或IDEA热部署导致。
解决:在方法中添加判断,避免重复初始化:
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
// 检查是否已初始化
if (context.getAttribute("SqlSessionFactory") != null) {
return; // 已存在,跳过
}
// 初始化代码...
}Q2: Listener中获取不到数据库连接?
原因:数据库驱动在Listener初始化时还未加载。
解决:确保数据库驱动JAR在WEB-INF/lib中,或使用延迟加载。
5.3 其他常见问题
Q3: 如何设置Filter的初始化参数?
注解方式:
@WebFilter(
value = "/*",
initParams = {
@WebInitParam(name = "encoding", value = "UTF-8"),
@WebInitParam(name = "forceEncoding", value = "true")
}
)
public class CharacterEncodingFilter implements Filter {
private String encoding;
@Override
public void init(FilterConfig config) throws ServletException {
this.encoding = config.getInitParameter("encoding");
}
}Q4: 如何排除特定路径不进行过滤?
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getRequestURI();
// 排除静态资源
if (path.startsWith("/static/") || path.endsWith(".js") || path.endsWith(".css")) {
chain.doFilter(request, response);
return;
}
// 继续过滤逻辑...
}6. 本章总复盘
6.1 三大组件协作图
┌─────────────────────────────────────────────────────────┐ │ 应用程序生命周期 │ ├─────────────────────────────────────────────────────────┤ │ 应用启动 │ │ ↓ │ │ ServletContext初始化 │ │ ↓ │ │ Listener.contextInitialized() ← 初始化全局资源 │ │ ↓ │ │ 等待请求... │ │ ↓ │ │ 请求到达 ───────────────────────────────────────┐ │ │ ↓ │ │ │ Filter1.doFilter()前半部分 │ │ │ ↓ │ │ │ Filter2.doFilter()前半部分 │ │ │ ↓ │ │ │ chain.doFilter() ───────────────────────────────┤───────┤ │ ↓ │ │ │ Servlet.service() │ │ │ ↓ │ │ │ Filter2.doFilter()后半部分 │ │ │ ↓ │ │ │ Filter1.doFilter()后半部分 ←────────────────────┘ │ │ ↓ │ │ 响应返回 │ │ ↓ │ │ 应用关闭 │ │ ↓ │ │ Listener.contextDestroyed() ← 清理资源 │ └─────────────────────────────────────────────────────────┘
6.2 知识点回顾
| 组件 | 核心接口 | 关键方法 | 主要用途 |
|---|---|---|---|
| Listener | ServletContextListener |
contextInitialized() contextDestroyed() |
应用启动/关闭时执行初始化和清理 |
| Filter | Filter |
init() doFilter() destroy() |
请求预处理、响应后处理 |
6.3 最佳实践
- 资源初始化:使用Listener在应用启动时初始化数据库连接池、缓存等
- 通用处理:使用Filter统一处理字符编码、登录验证、日志记录
- 顺序控制:多个Filter时使用web.xml精确控制执行顺序
- 异常处理:Filter中注意异常处理,避免影响正常请求
- 白名单机制:登录验证等场景使用白名单放行特定路径
<!-- [PRACTICE-START] 课后练习题已提取到 09-web-integration-practice.md -->