监听器 与 Filters
一、概述
监听器是用于监听Web应用而实现了特定接口的Java类。监听器可以在事件发生前、后做一些有必要的处理。
Servlet API提供了一系列的事件和事件监听接口。上层的servlet/JSP应用能够通过调用这些API进行事件驱动的开发。
这里监听的所有事件都继承自java.util.EventObject对象。
Servlet 中的监听器分类:
- 监听三个域对象(ServletContext、HttpSession、ServletRequest)的创建和销毁的三个监听器。
- 监听三个域对象的属性变更(添加、移除、替换)的三个监听器。
- 监听HttpSession中JavaBean的状态变换的监听(两个)。
Filters是拦截Request请求的对象: 在用户的请求访问资源前处理ServletRequest以及ServletResponse,它可以用于日志记录、解加密、Session检查、图像文件保护等。
二、监听器
2.1 监听器接口和注册
监听器接口主要在 javax.servlet 和 javax.servlet.http 的包中。对应上文的三类,有以下这些接口:
第一类:
名称 | 作用 |
javax.servlet.ServletContextListener | 它能响应Servlet生命周期事件,它提供了ServletContext创建之后和ServletContext关闭之前的会被调用的方法。 |
javax.servlet.http.HttpSessionListener | 它能响应HttpSession的创建、超时和失效事件。 |
javax.servlet.http.HttpActivationListener | 它在一个HttpSession激活或者失效时被调用。 |
javax.servlet.ServletRequestListener | 它能响应一个ServletRequest的创建或删除。 |
第二类:
名称 | 作用 |
javax.servlet.ServletContextAttributeListener | 它能响应ServletContext范围内的属性添加、删除、替换事件。 |
javax.servlet.http.HttpSessionAttributeListener | 它能响应HttpSession范围内的属性添加、删除、替换事件。 |
javax.servlet.http.HttpSessionBindingListener | 可以实现这个接口来保存HttpSession范围的属性。当有属性从HttpSession添加或者删除时,它能做出响应。 |
javax.servlet.ServletRequestListener | 它能响应ServletRequest范围的属性添加、删除、修改事件。 |
编写一个监听器,只需要写一个Java类来实现对应的监听器接口就可以了。在Servlet 3.0和Servlet 3.1中提供了两种注册监听器的方法。一种是使用WebListener注解。例如:
@WebListener public class ListenerClass implements ListenerInterface { }
第二种是在部署描述文档中增加一个listener元素。
<listener> <listener-class>fully-qualified listener class</listener-class> </listener>
2.2 ServletContext 监听器
2.2.1 ServletContextListener
当ServletContext 初始化时,容器会调用所有注册的ServletContextListeners 的 contextInitialized方法。
当ServletContext 将要销毁是,容器会调用所有注册的ServletContextListeners 的 contextDestroyed方法。
void contextInitialized(ServletContextEvent event) void contextDestroyed(ServletContextEvent event)
contextInitialized方法和contextDestroyed方法都会从容器获取到一个ServletContextEvent。
javax.servlet.ServletContextEvent 是一个 java.util.EventObject的子类,它定义了一个访问ServletContext的getServletContext方法。
2.2.2 ServletContextAttributeListener
当一个ServletContext范围的属性被添加、删除或者替换时,ServletContextAttributeListener接口的实现类会接受到消息。这个接口定义了三个方法:
void attributeAdded(ServletContextAttributeEvent event) void attributeRemoved(ServletContextAttributeEvent event) void attributeReplaced(ServletContextAttributeEvent event)
这三个方法都能获取到一个 ServletContextAttributeEvent 的对象,通过这个对象可以获取属性的名称和值。
2.3 Session Listeners
2.3.1 HttpSessionListener
当一个HttpSession创建或者销毁时,容器都会通知所有的HttpSessionListener监听器,HttpSessionListener接口有两个方法:sessionCreated 和 sessionDestroyed。
void sessionCreated(HttpSessionEvent event) void sessionDestroyed(HttpSessionEvent event)
这两个方法都可以接收到一个继承于 java.util.EventObject 的HttpSessionEvent 对象。可以通过调用HttpSessionEvent 对象的getSession方法来获取当前的HttpSession。
下面举一个例子,这个监听器可以用来统计HttpSession的数量。
package app08a.listener; import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; @WebListener public class SessionListener implements HttpSessionListener, ServletContextListener{ @Override public void contextInitialized(ServletContextEvent e){ ServletContext servletContext = e.getServletContext(); servletContext.setAttribute("userCount", new AtomicInteger()); } @Override public void contextDestroyed(ServletContextEvent e){ } @Override public void sessionCreated(HttpSessionEvent e){ HttpSession session = e.getSession(); ServletContext servletContext = session.getServletContext(); AtomicInteger userCounter = (AtomicInteger)servletContext.getAttribute("userCount"); int userCount = userCounter.incrementAndGet(); System.out.println("userCount incremented to :"+userCount); } public void sessionDestroyed(HttpSessionEvent e){ HttpSession session = e.getSession(); ServletContext servletContext = session.getServletContext(); AtomicInteger userCounter = (AtomicInteger)servletContext.getAttribute("userCount"); int userCount = userCounter.decrementAndGet(); System.out.println("---------- userCount decremented to :"+userCount); } }
当第一次访问页面时,控制台打印如下信息:
2.3.2 HttpSessionAttributeListener
HttpSessionAttributeListener接口有以下方法:
void attributeAdded(HttpSessionBindingEvent event) void attributeRemoved(HttpSessionBindingEvent event) void attributeReplaced(HttpSessionBindingEvent event)
这三个方法都能获取到一个 HttpSessionBindingEvent的对象,通过这个对象可以获取属性的名称和值。
由于HttpSessionBinding是 HttpSessionEvent的子类,因此也可以在HttpSessionAttributeListener实现类中获得HttpSession。
2.3.3 HttpSessionActivationListener
在分布式环境下,会用多个容器进行负载均衡,有可能需要将session保存起来,在容器之间传递。例如当一个容器内存不足时,会把很少用到的对象转存到其他容器上,这时候,容器就会通知所有HttpSessionActivationListener接口实现的类。
HttpSessionActivationListener接口有两个方法,sessionDidActivate 和 sessionWillPassivate。
void sessionDidActivate(HttpSessionEvent event) void sessionWillPassivate(HttpSessionEvent event)
当HttpSession被转移到其他容器之后,sessionDidActivate就会被调用。容器将一个HttpSession方法传递到方法里,可以从这个对象获得HttpSession。
2.3.4 HttpSessionBindingListener
当有属性绑定或者解绑到HttpSession上时,HttpSessionBindingListener 监听器就会被调用。
2.4 ServletRequest Listeners
2.4.1 ServletRequestListener
ServletRequestListener监听器会对ServletRequest的创建和销毁事件进行响应。容器通过一个池来存放并重复利用多个ServletRequest,ServletRequest的创建是从容器池里被分配出来的时刻开始,而它的销毁时刻是放回池里的事件。
ServletRequestListener接口有两个方法:
void requestInitialized(ServletRequestEvent event) void requestDestroyed(ServletRequestEvent event)
这两个方法都会接收到一个ServletRequestEvent对象,可以通过这个对象的getServletRequest方法来获取ServletRequest对象。
另外,ServletRequestEvent接口也提供了一个getServletContext方法来获取ServletContext。
2.4.2 ServletRequestAttributeListener
此接口提供了三个方法:
void attributeAdded(ServletRequestAttributeEvent event) void attributeRemoved(ServletRequestAttributeEvent event) void attributeReplaced(ServletRequestAttributeEvent event)
三、Filters
3.1 Filter API
下面介绍与Filter相关的接口,包含 Filter、FilterConfig、Filter Chain。
Filter的实现必须继承 javax.servlet.Filter 接口。这个接口包含了 Filter 的三个生命周期:init,doFilter,destroy。
Servlet容器初始化 Filter 时,会触发 Filter 的 init 方法,一般在应用开始时,而不是在相关资源使用时才初始化,这个方法只调用一次。init 定义如下:
void init(FilterConfig filterConfig)
当Servlet 容器每次处理Filter相关资源时,都会调用该Filter实例的doFilter方法。doFilter包含三个参数,定义如下:
void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
在doFilter中可以访问ServletRequest、ServletResponse。
在Filter的doFilter的实现中,最后一行需要调用FilterChain 中的 doFilter 方法。注意Filter的doFilter方法里的第三个参数,就是filterChain的实例:
filterChain.doFilter(request, response)
一个资源可能被多个Filter关联到(即Filter 链条),这时Filter.doFilter()的方法将会触发Filter链条中的下一个Filter。只有在Filter链条中最后一个Filter里调用的FilterChain.doFilter(),才会触发处理资源的方法。
如果在Filter,doFilter()的实现中,没有在结尾处调用FilterChain.doFilter() 的方法,那么该Request请求终止,后面的处理就会中断。
注意:FilterChain接口中,唯一的方法是doFilter。该方法与Filter中的doFilter方法的定义是不一样的,它只有两个参数。
Filter接口中,最后一个方法是destroy,定义如下:
void destroy()
该方法在Servlet容器要销毁Filter时触发,一般在应用停止的时候进行调用。
3.2 Filter配置
当完成Filter的实现后,就要进行配置Filter了。步骤如下:
- 确认哪些资源需要使用这个Filter拦截处理。
- 配置Filter的初始化参数值,这些参数可以在Filter的init方法中读取到:
- 给Filter取一个名称。一般来说,这个名称没什么特别含义。特殊的情况,例如要记录Filter的初始化时间,但这个应用中有许多的Filter,这时它就可以用来识别Filter了。
FilterConfig接口允许通过它的getServletContext的方法来访问ServletContext:
ServletContext getServletContext()
如果配置了 Filter 的名字,在FilterConfig 的getFilterName 中就可以获取Filter的名字。定义如下:
java.lang.String getFilterName()
当然,最重要还是要获取配置给Filter的初始化参数,需要用到FilterConfig中的两个方法,第一个方法是getParameterNames:
java.util.Enumeration<java.lang.String> getInitParametersNames()
这个方法返回Filter参数名字的Enumeration对象。如果没有给这个Filter配置任何参数,返回空。
第二个方法是getParameter:
java.lang.String getInitParameter(java.lang.String parameterName)
有两种方法可以配置Filter:一种是通过WebFilter的Annotation来配置Filter,另一种是通过部署描述来注册。下面是WebFilter的参数,所有参数都是可选的。
属性 | 描述 |
asyncSupported | Filter是否支持异步操作 |
description | Filter的描述 |
dispatcerTypes | Filter所生效范围 |
displayName | Filter的显示名 |
filterName | Filter的名称 |
initParams | Filter的初始化参数 |
largeIcon | Filter的大图名称 |
servletName | Filter所生效的Servlet名称 |
smallIcon | Filter的小图名称 |
urlPatterns | Filter所生效的URL路径 |
value | Filter所生效的URL路径 |
下面举个例子:
@WebFilter(filterName = "Security Filter", urlPatterns = {"/*"}, initParams = { @WebInitParam(name = "frequency", value = "1909"), @WebInitParam(name = "resolution", value = "1024") } }
如果使用部署描述符,那么对应的配置为:
<filter> <filter-name>Security Filter</filter-name> <filter-class>filterClass</filter-class> <init-param> <param-name>frequency</param-name> <param-value>1909</param-value> </init-param> <init-param> <param-name>resolution</param-name> <param-value>1024</param-value> </init-param> </filter> <filter-mapping> <filter-name>DateCompressionFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
3.3 示例1:日志Filter
package filter; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.util.Date; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.annotation.WebInitParam; import javax.servlet.http.HttpServletRequest; @WebFilter(filterName = "LoggingFilter", urlPatterns= {"/*"}, initParams = { @WebInitParam(name = "logFileName", value = "log.txt"), @WebInitParam(name = "prefix", value="URI: ") }) public class LoggingFilter implements Filter{ private PrintWriter logger; private String prefix; @Override public void init(FilterConfig filterConfig) throws ServletException{ prefix = filterConfig.getInitParameter("prefix"); String logFileName = filterConfig.getInitParameter("logFileName"); String appPath = filterConfig.getServletContext().getRealPath("/"); System.out.println("logFileName:" + logFileName); try{ logger = new PrintWriter(new File(appPath, logFileName)); }catch(FileNotFoundException e){ e.printStackTrace(); throw new ServletException(e.getMessage()); } } @Override public void destroy(){ System.out.println("destroying filter"); if(logger != null) logger.close(); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)throws IOException, ServletException{ System.out.println("LoggingFilter.doFilter"); HttpServletRequest httpServletRequest = (HttpServletRequest) request; logger.println(new Date() + "" + prefix + httpServletRequest.getRequestURL()); logger.flush(); filterChain.doFilter(request, response); } }
通过检查日志文件的内容,就可以验证这个Filter是否运行正常。
3.4 示例2:图像文件保护Filter
实现检查HTTP Header的referrer值,只有通过页面访问获取图片资源,直接输入文件路径无效。
package filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; @WebFilter(filterName = "ImageProtetorFilter", urlPatterns = {"*.png","*.jpg","*.gif"}) public class ImageProtectorFilter implements Filter{ @Override public void init(FilterConfig filterConfig) throws ServletException{ } @Override public void destroy(){} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)throws IOException, ServletException{ System.out.println("ImageProtectorFilter"); HttpServletRequest httpServletRequest = (HttpServletRequest) request; String referrer = httpServletRequest.getHeader("referer"); System.out.println("referrer:" + referrer); if(referrer!=null) filterChain.doFilter(request, response); else throw new ServletException("Image not available"); } }
3.5 示例3:下载计数Filter
package filter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; @WebFilter(filterName = "DownloadCounterFilter", url = {"/*"}) public class DownLoadCounterFilter implements Filter{ ExecutorService executorService = Executors.newSingleThreadExecutor(); Properties downloadLog; File logFile; @Override public void init(FilterConfig filterConfig) throws ServletException{ System.out.println("DownloadCounterFilter"); String appPath = filterConfig.getServletContext().getRealPath("/"); logFile = new File(appPath, "downloadLog.txt"); if(!logFile.exists()){ try{ logFile.createNewFile(); }catch(IOException e){ e.printStackTrace(); } } downloadLog = new Properties(); try{ downloadLog.load(new FileReader(logFile)); }catch(IOException e){ e.printStackTrace(); } } @Override public void destroy(){} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException{ HttpServletRequest httpServletRequest = (HttpServletRequest) request; final String uri = httpServletRequest.getRequestURI(); executorService.execute(new Runnable(){ @Override public void run(){ String property = downloadLog.getProperty(uri); if(property == null){ downloadLog.setProperty(uri, "1"); }else{ int count = 0; try{ count = Integer.parseInt(property); }catch(NumberFormatException e){ } count++; downloadLog.setProperty(uri, Integer.toString(count)); } try{ downloadLog.store(new FileWriter(logFile),""); }catch(IOException e){} } }); filterChain.doFilter(request, response); } }
3.6 Filter顺序
如果多个Filter应用于同一个资源,Filter的触发顺序将会很重要,需要使用部署描述符来管理Filter。举个例子,Filter1要在Filter2前被触发:
<filter> <filter-name>Filter1</filter-name> <filter-class>the fully-qualified name1</filter-class> </filter> <filter> <filter-name>Filter2</filter-name> <filter-class>the fully-qualified name2</filter-class> </filter>
如果需要保持或者改变Filter实现中的状态,就要考虑到线程安全问题。
<------ 天若有情天亦老,人间正道是沧桑 ------>