跳至主要內容

使用Filter和Listener

guodongAndroid大约 5 分钟

开发环境

本文升级了 IntelliJ IDEA 的版本

  • IntelliJ IDEA 2023.3.4 (Ultimate Edition)
  • JDK 11
  • Gradle 7.5.1
  • Kotlin 1.9.22
  • Tomcat 10.1.18

使用Filter

servlet list

目前,我们有以上几个 Servlet,现在我们想设置每个 Servlet 的输入和输出编码为 UTF-8。我们可以直接把设置编码的代码写在各个 Servlet 中,但是,同样的代码重复多次没有必要,并且,如果后续增加了新的 Servlet 还需要重复设置编码。

为了把一些公用逻辑从各个 Servlet 中抽离出来,JavaEE 的 Servlet 规范提供了一种 Filter 组件,即过滤器,它的作用是,在 HTTP 请求到达 Servlet 之前,可以被一个或多个 Filter 预处理,类似打印日志、登录检查等逻辑,完全可以放到 Filter 中。

现在,我们可以编写一个 EncodingFilter 将输入和输出编码设置为 UTF-8:

@WebFilter(urlPatterns = ["/*"])
class EncodingFilter : Filter {
    
    private val TAG = EncodingFilter::class.java.simpleName
    
    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        println("$TAG:doFilter")
        
        request.characterEncoding = StandardCharsets.UTF_8.name()
        response.characterEncoding = StandardCharsets.UTF_8.name()
        
        // 执行下一个过滤器
        chain.doFilter(request, response)
    }
}

编写 Filter 时必须实现 Filter 接口,然后 doFilter 方法里添加我们自己的逻辑,要继续处理请求必须调用 chain.doFilter(),否则请求会被拦截,所以 Filter 也可以作为拦截器使用。

最后,使用 @WebFilter 注解标识该类是一个 Filter 并通过 urlPatterns 参数指定该 Filter 需要过滤的 URL,这里指定 /* 表示过滤所有的路径。

添加了 Filter 后,请求的处理架构如下:

Filter

还可以继续添加其他的 Filter,比如 LogFilter

@WebFilter(urlPatterns = ["/*"])
// 					注意 ↓↓↓
class LogFilter : HttpFilter() {

    private val TAG = LogFilter::class.java.simpleName

    override fun doFilter(req: HttpServletRequest, res: HttpServletResponse, chain: FilterChain) {
        println("$TAG --> ${req.requestURI}")
        chain.doFilter(req, res)
    }
}

为了获取请求的路径,LogFilter 继承自 HttpFilter 而不是实现 Filter 接口,此时 doFilter() 方法中的形参类型为 HttpServlet*,即可调用 HttpServletRequest.requestURI 来获取请求的路径。

多个 Filter 会组成一个链,每个请求都会被链上的 Filter 依次处理:

filter chain

有多个 Filter 的时候如何指定顺序?多个 Filter 按不同的顺序处理会造成不同的结果么?

答案是 Filter 的顺序的确会对结果有影响。但是,Servlet 没有对 @WebFilter 注解标识的 Filter 规定顺序。如果一定要给 Filter 指定顺序,可以在 web.xml 文件中按顺序再配置一遍 Filter。

注意上述两个 Filter 的过滤路径都是 /*,即它们会过滤所有请求。我们也可以编写过滤指定路径的 Filter,例如 AuthFilter

@WebFilter(urlPatterns = ["/logout"])
class AuthFilter : HttpFilter() {

    private val TAG = AuthFilter::class.java.simpleName

    override fun doFilter(req: HttpServletRequest, res: HttpServletResponse, chain: FilterChain) {
        println("$TAG: check authentication")

        // 未登录,跳转到登录界面
        if (req.session.getAttribute("user") == null) {
            println("$TAG: not login")
            res.sendRedirect("/login")
        } else {
            // 已登录,继续处理
            chain.doFilter(req, res)
        }
    }
}

AuthFilter 指定过滤路径为 /logout,登出时必须先登录。若没有登录,则跳转到登录界面,否则继续正常处理。

以下是未登录时的输出:

LogoutServlet init
AuthFilter: check authentication
AuthFilter: not login
LoginServlet init
EncodingFilter:doFilter
LogFilter --> /login

登录后再登出的输出:

AuthFilter: check authentication
EncodingFilter:doFilter
LogFilter --> /logout
EncodingFilter:doFilter
LogFilter --> /index

注意观察 AuthFilter,当用户没有登录时,在 AuthFilter 内部,直接调用 res.sendRedirect() 发送重定向,且没有调用 chain.doFilter(),因此,当用户没有登录时,请求到达 AuthFilter 后,不再继续处理,即后续的 Filter 和任何 Servlet 都没有机会处理该请求了。

由此可见,Filter 可以有针对性的拦截或者放行请求。

如果一个 Filter 被执行,但是没有调用 FilterChain.doFilter() 方法:

@WebFilter(urlPatterns = ["/*"])
class TodoFilter : Filter {
    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        // Do nothing
        // 所有请求将被拦截
    }
}

那么,客户端将看到一个空白页面,因为请求被拦截而没有继续处理,此时默认的响应码是 200 并且没有响应体。

提示

要继续处理请求必须调用 chain.doFilter()

注解标识的 Filter 的顺序

前面说 <Servlet 没有对 @WebFilter 注解标识的 Filter 规定顺序>,经过断点调试和少量测试,发现默认是按文件名称排序的,代码顺序如下:

ContextConfig.webConfig() ──┐
	ContextConfig.processClasses() ──┐
		ContextConfig.processAnnotationsWebResource() ──┐
    		StandardRoot.listResources() ──┐
    			StandardRoot.list()

其实就是查找 /WEB-INF/classes 目录下的 classes 文件,而 classes 文件默认按文件名称排序。

使用Listener

除了 Filter 外,Servlet 还提供了 Listener,即监听器。Servlet 提供了多种 Listener,其中最常用的就是 ServletContextListener,它可以理解为 WebApp 的生命周期监听器:

@WebListener
class WebAppListener : ServletContextListener {

    private val TAG = WebAppListener::class.java.simpleName

    override fun contextInitialized(sce: ServletContextEvent) {
        println("$TAG: contextInitialized --> ${sce.servletContext}")
    }

    override fun contextDestroyed(sce: ServletContextEvent) {
        println("$TAG: contextDestroyed --> ${sce.servletContext}")
    }
}

使用 WebListener 注解标识当前类是 Listener。contextInitialized 会在 WebApp 初始化完成后调用,此时我们可以初始化一些资源,比如初始化数据库连接池等;contextDestroyed 会在 WebApp 关闭后调用,此时我们可以清理资源,比如关闭数据库连接等。

除了 ServletContextListener 外,还有几种 Listener:

  • HttpSessionListener:监听 HttpSession 的创建和销毁事件;
  • HttpSessionIdListener:监听 HttpSession Id 变更事件;
  • HttpSessionAttributeListener:监听 HttpSession 属性变化事件(即调用 HttpSession.setAttribute() 方法);
  • ServletRequestListener:监听 ServletRequest 请求的创建和销毁事件;
  • ServletRequestAttributeListener:监听 ServletRequest 请求的属性变化事件(即调用 ServletRequest.setAttribute() 方法);
  • ServletContextAttributeListener:监听 ServletContext 的属性变化事件(即调用 ServletContext.setAttribute() 方法);
  • 等等。

ServletContext

一个 Web 服务器可以运行一个或多个 WebApp,Web 服务器会为每个 WebApp 创建一个全局唯一的 ServletContext 实例,我们在 WebAppListener 监听器里面实现的两个回调方法实际上与 ServletContext 实例的创建与销毁一一对应。

总结

  • Filter 是对请求的一种预处理机制,它可以组成 Filter 链,
  • Filter 适用于日志收集、授权检查、全局设置等场景,
  • 为 Filter 指定最小的路径映射范围可以提高程序性能,同时也可以让 Filter 链更清晰,
  • Listener 可以监听 Servlet 的各种事件,其中最常用的是 ServletContextListener