Servlet之过滤器与侦听器

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 三大域对象(ServletContextHttpSessionServletRequest)的创建、销毁以及属性变化。

监听器接口 监听目标 触发时机 常见应用场景
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。 浏览器关闭后,客户端的 JSESSIONID Cookie 随之消失,但服务端的 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
    • 场景:注销、校验登录态等。

为什么注销要用 false

如果用户本身就没有 Session(可能已经超时或从未登录),我们没必要为了"销毁"这个动作而特地创建一个新 Session 出来(既浪费内存,又会错误触发生命周期监听器)。使用 false 可以优雅地判断"有房才退房,没房不打扰"。

相关推荐
奔跑的呱呱牛18 小时前
GeoJSON 在大数据场景下为什么不够用?替代方案分析
java·大数据·servlet·gis·geojson
huohuopro2 天前
Servlet概述
servlet
恼书:-(空寄2 天前
拦截器获取不到 POST 请求 JSON 结构体参数(完整解决方案)
java·spring boot·spring·servlet
_BugMan4 天前
【SSE】
java·servlet·tomcat
虚拟世界AI5 天前
Java服务器开发:零基础实战指南
java·servlet·tomcat
爱敲代码的菜菜5 天前
【项目】基于正倒排索引的Java文档搜索引擎
java·开发语言·前端·javascript·搜索引擎·servlet
清空mega7 天前
第7章:JavaBean、Servlet 与 MVC——从 JSP 页面开发走向规范项目
java·servlet·mvc
huohuopro7 天前
idea配置servlet项目
java·servlet·intellij-idea
沉默-_-7 天前
接收请求:HttpServletRequest的几种用法
前端·servlet·firefox