Servlet之过滤器与侦听器
过滤器JavaWeb三大组件之一,它与Servlet很相似!过滤器是用来拦截请求的,而不是处理请求的。
单个过滤器示意图:

多个过滤器示意图:

1前 2前 2后 1后
过滤器 (Filter)
核心面试点
- 核心设计思想:基于责任链模式。多个过滤器形成链条,必须显式调用放行方法,请求才会继续向后传递。
- 与拦截器对比:过滤器是 Servlet 规范,基于函数回调,作用于 Web 容器,能够拦截所有请求;拦截器是 Spring 框架组件,基于反射与 AOP,仅拦截到达 Controller 的请求。
- 执行顺序:在 web.xml 中由 mapping 声明顺序决定;在 Spring Boot 中可通过注册 Bean 指定顺序。
- 放行机制:若在 doFilter 方法中未调用 chain.doFilter(),则请求会被直接拦截,不会到达目标资源。
生命周期与核心方法
Filter 用于在请求到达目标资源(如 Servlet、JSP)之前或响应返回客户端之前,进行拦截和处理。与 Servlet 类似,它由 Web 容器(如 Tomcat)管理生命周期,且为单例。
| 方法名称 | 执行时机 | 作用说明 |
|---|---|---|
init(FilterConfig) |
容器启动时执行一次 | 初始化过滤器,读取相关配置参数。 |
doFilter(...) |
每次请求匹配时执行 | 核心业务逻辑。必须调用 filterChain.doFilter() 才能放行请求。 |
destroy() |
容器关闭时执行一次 | 释放资源,处理收尾工作。 |
url-pattern 匹配规则
在配置过滤器拦截路径时,匹配规则与 Servlet 一致:
- 完全路径匹配:以
/开头,如/user/login。 - 目录匹配:以
/开头,以*结尾,如/*(拦截所有)或/admin/*。 - 扩展名匹配:不能以
/开头,如*.do、*.action。
FilterChain (过滤链) 与执行顺序
当配置了多个 Filter 时,它们的执行顺序由 web.xml 中 <filter-mapping> 的声明顺序决定。
- 放行前:在请求到达目标资源前执行,常用于预处理与权限校验。
- 放行后:在目标资源执行完毕、响应返回客户端前执行,常用于统一处理响应数据。
java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 请求预处理 (如:设置字符编码、校验登录态)
System.out.println("执行目标资源前...");
// 放行 (执行下一个 Filter 或目标资源)
chain.doFilter(request, response);
// 响应后处理 (如:包装响应结果)
System.out.println("目标资源执行完毕后...");
}
经典应用场景
- 统一字符集编码:解决请求与响应的中文乱码问题。
- 权限访问控制:拦截敏感路径,校验用户会话或鉴权令牌是否存在。
- 敏感词过滤:拦截用户输入参数并替换违规内容后再放行。
- 跨域处理:在响应头中统一添加 CORS 相关的跨域允许字段。
登录鉴权与耗时统计
过滤器的放行与拦截机制(鉴权),以及利用执行前后切面特性计算接口耗时。
创建耗时统计过滤器,利用放行前后的时间差计算耗时:
java
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class TimeLogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
long startTime = System.currentTimeMillis();
// 调用放行方法,使得请求可以到达下一个过滤器或后续阶段
chain.doFilter(request, response);
long endTime = System.currentTimeMillis();
HttpServletRequest httpRequest = (HttpServletRequest) request;
System.out.println("请求路径: " + httpRequest.getRequestURI() + ",总耗时: " + (endTime - startTime) + "ms");
}
@Override
public void destroy() {}
}
创建鉴权过滤器,演示如何阻断非法请求:
java
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class AuthFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {}
@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();
// 放行登录等公开接口
if (requestURI.contains("/login")) {
chain.doFilter(request, response);
return;
}
// 校验登录状态,若不符合条件则直接拦截,不再执行 chain.doFilter
Object user = httpRequest.getSession().getAttribute("USER_SESSION");
if (user == null) {
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("UnAuthorized - 拒绝访问");
// 请求到此结束,不会继续向后传递
return;
}
// 校验通过,正常放行
chain.doFilter(request, response);
}
@Override
public void destroy() {}
}
配套配置文件示例,明确过滤器的执行顺序:
xml
<!-- 耗时过滤器先声明,处于责任链最外层,包裹整个请求流程 -->
<filter>
<filter-name>timeLogFilter</filter-name>
<filter-class>com.example.TimeLogFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>timeLogFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 鉴权过滤器后声明,仅保护特定资源 -->
<filter>
<filter-name>authFilter</filter-name>
<filter-class>com.example.AuthFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>authFilter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
监听器 (Listener)
核心面试点
- 核心设计思想:基于观察者模式(事件驱动模型)。监听器作为观察者,域对象作为被观察者。
- 框架整合原理:Spring 框架中的 ContextLoaderListener 实现了 ServletContextListener。在 Web 容器启动触发 contextInitialized 时加载 IoC 容器,是面试中常考的框架整合底层原理。
- 线程安全问题:在使用监听器操作全局对象(如 ServletContext)时,由于处于多线程环境,必须考虑并发修改引发的线程安全隐患。
核心接口与场景
Listener 用于监听 Java Web 三大域对象(ServletContext、HttpSession、ServletRequest)的创建、销毁以及属性变化。
| 监听器接口 | 监听目标 | 触发时机 | 常见应用场景 |
|---|---|---|---|
ServletContextListener |
全局应用 (Application) | 容器启动与关闭 | 加载全局配置、初始化单例资源、启动 Spring 容器 |
HttpSessionListener |
用户会话 (Session) | 会话创建与销毁 | 统计在线人数、清理用户缓存 |
ServletRequestListener |
单次请求 (Request) | 请求到达与结束 | 审计请求日志 |
高并发下的在线人数统计
本案例演示监听器在统计在线人数中的应用,并引入面试中常考的并发安全处理方案,摒弃普通的同步锁,改用 JDK 提供的原子类解决多线程下的数据一致性问题。
java
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.concurrent.atomic.AtomicInteger;
public class OnlineUserListener implements ServletContextListener, HttpSessionListener {
// Web 环境是典型的多线程环境,必须使用线程安全的数据结构
// AtomicInteger 采用 CAS 机制保证原子性,性能优于传统加锁方式
private static final String ONLINE_COUNT_KEY = "global_online_count";
@Override
public void contextInitialized(ServletContextEvent sce) {
// 容器启动时,初始化原子变量并存入全局上下文
ServletContext context = sce.getServletContext();
context.setAttribute(ONLINE_COUNT_KEY, new AtomicInteger(0));
System.out.println("全局环境初始化完成,在线人数计数器已装载。");
}
@Override
public void sessionCreated(HttpSessionEvent se) {
// Session 被创建代表有新访客,利用原子特性安全递增
ServletContext context = se.getSession().getServletContext();
AtomicInteger count = (AtomicInteger) context.getAttribute(ONLINE_COUNT_KEY);
if (count != null) {
int current = count.incrementAndGet();
System.out.println("用户上线,当前在线人数: " + current);
}
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
// Session 销毁(超时或手动登出)代表访客离开,进行递减
ServletContext context = se.getSession().getServletContext();
AtomicInteger count = (AtomicInteger) context.getAttribute(ONLINE_COUNT_KEY);
if (count != null) {
int current = count.decrementAndGet();
System.out.println("用户离线,当前在线人数: " + current);
}
}
}
!IMPORTANT
关闭浏览器 ≠ 销毁 Session。 浏览器关闭后,客户端的
JSESSIONIDCookie 随之消失,但服务端的 Session 对象仍驻留在内存中,直到超时才会被容器回收并触发sessionDestroyed。Tomcat 默认超时时间为 30 分钟。这意味着关闭浏览器后,在线人数不会立刻减少。
缩短超时时间(便于验证效果):
xml
<session-config>
<session-timeout>1</session-timeout> <!-- 单位:分钟 -->
</session-config>
主动注销方案(立即触发 sessionDestroyed):
java
// LogoutServlet ------ 映射到 /logout
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
HttpSession session = req.getSession(false);
if (session != null) {
session.invalidate(); // 立即销毁 Session,触发监听器的 sessionDestroyed
}
resp.getWriter().write("已登出");
}
在 Logout 流程中,我们使用了 request.getSession(false),这在面试中常被问到原因:
getSession(true)(默认值):- 逻辑 :如果有 Session 则返回;若没有,则强制创建一个。
- 场景:登录、存取购物车等需要保证 Session 对象必然存在的场景。
getSession(false):- 逻辑 :如果有 Session 则返回;若没有,则直接返回
null。 - 场景:注销、校验登录态等。
- 逻辑 :如果有 Session 则返回;若没有,则直接返回
为什么注销要用 false?
如果用户本身就没有 Session(可能已经超时或从未登录),我们没必要为了"销毁"这个动作而特地创建一个新 Session 出来(既浪费内存,又会错误触发生命周期监听器)。使用 false 可以优雅地判断"有房才退房,没房不打扰"。