Filter 与 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>
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");③ 数据库层编码没对齐。三层都要查。