使用Filter和Listener
开发环境
本文升级了 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,现在我们想设置每个 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,比如 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 的时候如何指定顺序?多个 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
。