楼层: 首页/ 软件技术/ JavaEE / Jakarta EE/ Filter 与 Listener:拦路虎与观察员
06

Filter 与 Listener:拦路虎与观察员

Filter Chain · Character Encoding · Listener

每个请求都做一遍的事——"检查登录没有""统一设编码""记录访问日志"——总不能在每个 Servlet 里抄一遍。Filter(过滤器)就是请求到达 Servlet 之前先过的一道安检门;Listener(监听器)是网站启动、销毁、用户上线下线时自动触发的钩子。

字符编码过滤器:治中文乱码的标配(Filter 链)

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")   // 拦所有请求
public class EncodingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp,
                          FilterChain chain) throws IOException, ServletException {
        req.setCharacterEncoding("UTF-8");    // 统一请求编码
        resp.setContentType("text/html;charset=UTF-8");
        chain.doFilter(req, resp);             // 放行,交给下一个 Filter / Servlet
    }
}

登录验证过滤器:没登录就踹回登录页

@WebFilter("/admin/*")   // 只拦 /admin/ 开头的后台路径
public class LoginFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp,
                          FilterChain chain) throws IOException, ServletException {
        HttpServletRequest hreq = (HttpServletRequest) req;
        Object user = hreq.getSession().getAttribute("loginUser");
        if (user == null) {
            ((HttpServletResponse) resp).sendRedirect("/login.jsp");
            return;                     // 不放行,请求到此为止
        }
        chain.doFilter(req, resp);
    }
}
常用 Listener什么时候触发
ServletContextListener网站启动 / 关闭时各调一次——用来初始化连接池、加载缓存。
HttpSessionListener有用户建 Session / 销毁 Session 时触发——统计在线人数。
ServletRequestListener每个请求开始 / 结束——埋访问日志。

Filter 链的执行顺序

一个请求可能被多个 Filter 拦:先 EncodingFilter 设编码,再 LoginFilter 查登录。顺序由 @WebFilter 的顺序(或 @Order)决定,先配置的先执行,响应时反过来走。记不住就记住:编码 Filter 永远放最前,不然后面的 Filter 已经用错误编码读了参数。

ServletContextListener:网站启动时建连接池,关闭时关掉

@WebListener
public class AppLifecycleListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // 网站一启动,就把连接池建好,放 application 域里全局共享
        HikariDataSource ds = /* 见第 5 章配置 */;
        sce.getServletContext().setAttribute("ds", ds);
        System.out.println(">> 连接池已就绪");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // 网站关闭时优雅关掉连接池,别让连接泄漏到数据库那边
        HikariDataSource ds = (HikariDataSource) sce.getServletContext().getAttribute("ds");
        ds.close();
    }

HttpSessionListener:统计在线人数

@WebListener
public class OnlineCounter implements HttpSessionListener {
    // ConcurrentHashMap 线程安全,容器可能并发建/毁 Session
    private static final Set<String> ONLINE = ConcurrentHashMap.newKeySet();

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        ONLINE.add(se.getSession().getId());
        System.out.println("在线人数:" + ONLINE.size());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        ONLINE.remove(se.getSession().getId());
    }
}

完整组合案例:Filter 链 + Listener + Servlet 全链路(注解版)

// ① EncodingFilter:最前面,统一编码(Filter 链第一环)
@WebFilter("/*")
public class EncodingFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html;charset=UTF-8");
        chain.doFilter(req, resp);     // 放行给下一个 Filter
    }
}

// ② LoginFilter:检查登录,没登录踹回登录页(Filter 链第二环)
@WebFilter("/admin/*")
public class LoginFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest hreq = (HttpServletRequest) req;
        if (hreq.getSession().getAttribute("user") == null) {
            ((HttpServletResponse) resp).sendRedirect("/login.jsp");
            return;               // 不放行,请求到此为止
        }
        chain.doFilter(req, resp);
    }
}

// ③ AppListener:网站启动时建连接池,关闭时销毁(Listener)
@WebListener
public class AppListener implements ServletContextListener {
    public void contextInitialized(ServletContextEvent sce) {
        HikariDataSource ds = /* 见第 5 章 */;
        sce.getServletContext().setAttribute("ds", ds);
    }
    public void contextDestroyed(ServletContextEvent sce) {
        ((HikariDataSource) sce.getServletContext().getAttribute("ds")).close();
    }
}

// ④ AdminServlet:在 /admin/* 下,被两个 Filter 保护
@WebServlet("/admin/dashboard")
public class AdminServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.getWriter().write("欢迎来到后台," + req.getSession().getAttribute("user"));
    }
}

// 请求链路:浏览器 → EncodingFilter(设编码) → LoginFilter(查登录)
//          → AdminServlet(写后台页面) → 原路返回

web.xml 配置版(老项目可能遇到,注解版不用写)

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" version="6.0">
  <!-- Filter 配置 -->
  <filter>
    <filter-name>EncodingFilter</filter-name>
    <filter-class>com.example.EncodingFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>EncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- Servlet 配置 -->
  <servlet>
    <servlet-name>admin</servlet-name>
    <servlet-class>com.example.AdminServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>admin</servlet-name>
    <url-pattern>/admin/dashboard</url-pattern>
  </servlet-mapping>
</web-app>
章末面试 · Filter 与 Listener(4 题)

1.(概念题)Filter 链执行顺序?一个请求经过几个 Filter?

查看答案

答案:先过所有匹配的 Filter(按配置顺序),最后才到 Servlet;响应回来时 Filter 倒着走。chain.doFilter() 是放行点。

2.(排错题)EncodingFilter 为什么必须放最前面?

查看答案

答案:如果 LoginFilter 先跑,它已经用错误编码读了 request 参数,EncodingFilter 再设编码也晚了。所以编码 Filter 永远第一。

3.(概念题)Listener 有哪几种?分别什么时候触发?

查看答案

答案:ServletContextListener(网站启动/关闭)、HttpSessionListener(Session 创建/销毁,统计在线人数)、ServletRequestListener(每个请求开始/结束,埋日志)。

4.(排错题)Filter 里设了编码还是乱码?

查看答案

答案:① Filter 顺序不对,编码 Filter 没在最前;② 响应没设 setContentType("text/html;charset=UTF-8");③ 数据库层编码没对齐。三层都要查。