JSP Filters(过滤器)
Filter是拦截Request请求的对象:在用户的请求访 问资源前处理ServletRequest以及ServletResponse,它可 用于日志记录、加解密、Session检查、图像文件保护 等。通过Filter可以拦截处理某个资源或者某些资源。 Filter的配置可以通过Annotation或者部署描述来完成。 当一个资源或者某些资源需要被多个Filter所使用到, 且它的触发顺序很重要时,只能通过部署描述来配置。
一.Filter API
1. Filter 相关接口,包含Filter, FilterConfig, FilterChain
Filter的实现必须继承javax.servlet.Filter接口。这个 接口包含了Filter的3个生命周期:init、doFilter、 destroy。
Servlet容器初始化Filter时,会触发Filter的init方 法,一般来说是在应用开始时。也就是说,init方法并 不是在该Filter相关的资源使用到时才初始化的,而且 这个方法只调用一次,用于初始化Filter。init方法的定 义如下:
oid init(FilterConfig filterConfig)
注意: FilterConfig实例是由Servlet容器传入init方法中的.
当Servlet容器每次处理Filter相关的资源时,都会调 用该Filter实例的doFilter方法。Filter的doFilter方法包含 ServletRequest、ServletResponse、FilterChain这3个参 数。
doFilter的定义如下:
void doFilter(ServletRequest request, ServletResponse response,FilterChain filterChain)
接下来,说明一下doFilter的实现中访问 ServletRequet、ServletResponse。这也就意味着允许给 ServletRequest增加属性或者增加Header。当然也可以修 饰ServletRequest或者ServletRespone来改变它们的行 为。
在Filter的doFilter的实现中,最后一行需要调用 FilterChain中的doFilter方法。注意Filter的doFilter方法 里的第3个参数,就是filterChain的实例:
filterChain.doFilter(request, response)
一个资源可能需要被多个Filter关联到(更专业一 点来说,这应该叫作Filter链条),这时Filter.doFilter() 的方法将触发Filter链条中下一个Filter。只有在Filter链 条中最后一个Filter里调用的FilterChain.doFilter(),才会 触发处理资源的方法。
如果在Filter.doFilter()的实现中,没有在结尾处调 用FilterChain.doFilter()的方法,那么该Request请求中 止,后面的处理就会中断。
注意: FilterChain接口中,唯一的方法就是doFilter。该方法与Filter中的 doFilter的定义是不一致的:在FilterChaing中,doFilter方法只有两个参 数,但在Filter中,doFilter方法有三个参数。
Filter接口中,最后一个方法是destroy,它的定义 如下:
Void destroy()
该方法在Servlet容器要销毁Filter时触发,一般在应 用停止的时候进行调用。
除非Filter在部署描述中被多次定义到,否则Servlet 窗口只会为每个Filter创建单一实例。由于Serlvet/JSP的 应用通常要处理用户并发请求,此时Filter实例需要同 时被多个线程所关联到,因此需要非常小心地处理多线 程问题。
三. Filter配置
当完成Filter的实现后,就可以开始配置Filter了。 Filter的配置需要如下步骤:
- 确认哪些资源需要使用这个Filter拦截处理。
- 配置Filter的初始化参数值,这些参数可以在Filter的 init方法中读取到;
- 给Filter取一个名称。一般来说,这个名称没有什么 特别的含义,但在一些特殊的情况下,这个名字十 分有用。例如,要记录Filter的初始化时间,但这个 应用中有许多的Filter,这时它就可以用来识别Filter 了。
FilterConfig接口允许通过它的getServletContext的 方法来访问ServletContext:
ServletContext getServletContext(
如果配置了Filter的名字,在FilterConfig的 getFilterName中就可以获取Filter的名字。getFilterName 的定义如下:
java.lang.String getFilterName()
当然,最重要的还是要获取到开发者或者运维给 Filter配置的初始化参数。为了获取这些初始化参数, 需要用到FilterConfig中的两个方法,第一个方法是 getParameterNames:
java.util.Enumeration<java.lang.String> getInitParameterNames()
这个方法返回Filter参数名字的Enumeration对象。 如果没有给这个Filter配置任何参数,该方法返回的是 空的Enumeration对象。
第二个方法是getParameter:
java.lang.String getInitParameter(java.lang.String parameterName)
有两种方法可以配置Filter:一种是通过WebFilter 的Annotation来配置Filter,另一种是通过部署描述来注 册。使用@WebFilter的方法,只需要在Filter的实现类 中增加一个注解即可,不需要重复地配置部署描述。当 然,此时要修改配置参数,就需要重新构建Filter实现 类了。换句话说,使用部署描述意味着修改Filter配置 只要修改一下文本文件就可以了。
使用@WebFilter,你需要熟悉下表中所列出来的 参数,这些参数是在WebFilter的Annotation里定义的。 所有参数都是可选的。
属性 | 描述 |
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路径 |
三. 示例1: 日志 Filter
作为第1个例子,将做一个简单的Filter:在app09a 的应用中把Request请求的URL记录到日志文本文件 中。日志文本文件名通过Filter的初始化参数来配置。 此外,日志的每条记录都会有一个前缀,该前缀也由 Filter初始化参数来定义。通过日志文件,可以获得许 多有用的信息,例如在应用中哪些资源访问最频繁; Web站点在一天中的哪个时间段访问量最多。
这个Filter的类名叫LoggingFilter。 一般情况下,Filter的类名都以*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"); //得到URI String logFileName = filterConfig.getInitParameter("logFileName"); String appPath = filterConfig.getServletContext().getRealPath("/"); //得到项目路径 // without path info in logFileName, the log file will be // created in $TOMCAT_HOME/bin System.out.println("logFileName:" + logFileName); try { logger = new PrintWriter(new File(appPath, logFileName)); //打开文件 logger.println("I have the output"); logger.flush(); } 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.getRequestURI()); //写入数据 logger.flush(); //刷新缓冲 filterChain.doFilter(request, response); //如果没有doFilter方法,后面发Filter处理就会中断 } }
下面来仔细分析一下Filter类。 首先,该Filter的类实现了Filter的接口并声明两个 变量:PrintWriter类型的logger和String类型的prefix。
其中PrintWriter用于记录日志到文本文件,prefix的 字符串用于每条日志的前缀。 Filter的类使用了@WebFilter的Annotation,将两个 参数(logFilteName、prefix)传入到该Filter中.
在Filter的init方法中,通过FilterConfig里传入的 getInitParameter方法来获取prefix和getFileName的初始 化参数。其中把prefix参数中赋给了类变量prefix, logFileName则用于创建一个PrintWriter
如果Servlet/JSP应用是通过Servlet/JSP容器启动 的,那么当前应用的工作目录是当前JDK所在的目录。 如果是在Tomcat中,该目录是Tomcat的安装目录。在 应用中创建日志文件,可以通过 ServletContext.getRealPath来获取工作目录,结合应用 工作目录以及初始化参数中的logFilterNmae,就可以得 到日志文件的绝对路径.
当Filter的init方法被执行时,日志文件就会创建出 来。如果在应用的工作目录中该文件已经存在,那么该 日志文件的内容将会被覆盖。 当应用关闭时,PrintWriter需要被关闭。因此在 Filter的destroy方法中,需要:关闭日志
Filter的doFilter实现中记录着所有从ServletRequest 到HttpServletRequest的Request,并调用了它的 getRequestURI方法,该方法的返回值将记录通过 PrintWriter的pringln记录下来
每条记录都有一个时间戳以及前缀,这样可以很方 便地标识每条记录。接下来 Filter的doFilter实现调用 PrintWriter的flush方法以及FilterChain.doFilter,以唤起 资源的调用:
如果使用Tomcat,Filter的初始化并不会等到第一 个Request请求时才触发进行。这点可以在控制台中打 印出来的logFileName参数值中可以看到。在app09a应 用中通过URL调用test.jsp页面,就可以测试该Filter了
通过检查日志文件的内容,就可以验证这个Filter 是否运行正常。
四. 示例2:图像文件保护Filter
本例中的图像文件保护Filter用于在浏览器中输入 图像文件的URL路径时,防止下载图像文件。应用中的 图像文件只有当图像链接在页面中被点击的时候才会显 示。该Filter的实现原理是检查HTTP Header的referer 值。如果该值为null,就意味着当前的请求中没有 referer值,即当前的请求是直接通过输入URL来访问该 资源的。如果资源的Header值为非空,将返回Request 语法的原始页面作为referer值。注意Header的referer的 属性名中,在第2个e以及第3个e中仅有一个r。
ImageProtectorFilter的Filter实现类,如清单所 示。从WebFilter的Annotation中,可以看到该Filter应用 于所有的.png、.jpg、.gif文件后缀。
ImageProtectorFilter实现类
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; //过滤所有以.png , .jpg .gif 为后缀的访问 @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 { //直接输入图片地址访问http://localhost:8080/app09/图片.jpg会被拦截 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"); } } }
这里并没有init和destroy方法。其中doFilter方法读 取到Header中的referer值,要确认是要继续处理这个资 源还是给个异常.
测试该Filter,可以在浏览器中输入如下ULR路 径,尝试访问logo.png图像:
http://localhost:8080/app09/图片.jpg
接下来,通过image.jsp的页面来访问该图像:
http://localhost:8080/app09/test.jsp
五.示例3:下载计数Filter
本例子中,下载计数Filter将会示范如何在Filter中 计算资源下载的次数。这个示例特别有用,它将会得到 文档、音频文件的受欢迎程度。作为简单的示例,这里 将数值保存在属性文件中,而不保存在数据库中。其中 资源的ULR路径将作为属性名保存在属性文件中。
因为我们把值保存在属性文件中,并且Filter可以 被多线程访问,因此涉及线程安全问题。用户访问一个 资源时,Filter需要读取相应的属性值加1,然后保存该 值。如果第二个用户在第一个线程完成前同时访问该资 源,将会发生什么呢?计算值出错。在本例中,读写的 同步锁并不是一个好的解决这个问题的方法,因为它会 导致扩展性问题。
本示例中,解决这个线程安全问题是通过Queue以 及Executor。
简而言之,进来的Request请求将会保存在单线程 Executor的队列中。替换这个任务十分方便,因为这是 一个异步的方法,因此你不需要等待该任务结束。 Executor一次从队列中获取一个对象,然后做相应属性 值的增加。由于Executor只在一个线程中使用,因此可 以消除多个线程同时访问一个属性文件的影响。
DownloadCounterFilter实现类
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; // 拦截所有URL //书上的是 urlPatterns={"/"},亲测无效 @WebFilter(filterName = "DownloadCounterFilter", urlPatterns = { "*.JPG" }) public class DownloadCounterFilter implements Filter { // 获取单个线程池的Executor 由于Executor只在一个线程中使用,因此可 以消除多个线程同时访问一个属性文件的影响。 ExecutorService executorService = Executors.newSingleThreadExecutor(); // propeties 属性集 Properties downloadLog; File logFile; @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("DownloadCounterFilter"); //获取路径 String appPath = filterConfig.getServletContext().getRealPath("/"); // 获取downLoadLog.txt文件 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() { executorService.shutdown(); } @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) { // silent } count++; downloadLog.setProperty(uri, Integer.toString(count)); } try { downloadLog.store(new FileWriter(logFile), ""); } catch (IOException e) { } } }); filterChain.doFilter(request, response); } }
如果在当前应用的工作目录中不存在 downloadLog.txt文件,这个Filter的init方法就会创建 它
接着创建Properties对象,并读取该文件.
注意,Filter的实现类中引用到了 ExecutorService(Executor的子类).
且当Filter销毁时,会调用ExecutorService的 shutdown方法.
Filter的doFilter实现中大量地使用到这个Job。每次 URL请求都会调用到ExecutorService的execute方法,然 后才调用FilterChaing.doFilter()。该任务的execute实现 非常好理解:它将URL作为一个属性名,从Properties 实例中获取该属性的值,然后加1,并调用flush方法写 回到指定的日志文件中
这个Filter可在许多资源上生效,但也可以非常简 单地配置,限定为PDF或者AVI文件资源。
properties文件
六. Filter顺序
如果多个Filter应用于同一个资源,Filter的触发顺 序将变得非常重要,这时就需要使用部署描述来管理 Filter:指定哪个Filter先被触发。例如:Filter 1需要在 Filter 2前被触发,那么在部署描述中,Filter 1需要配置 在Filter 2之前:
<filter> <filter-name>Filter1</filter-name> <filter-class> the fully-qualified name of the filter class </filter-class> </filter> <filter> <filter-name>Filter2</filter-name> <filter-class> the fully-qualified name of the filter class </filter-class> </filter>
通过部署描述之外的配置来指定Filter触发的顺序 是不可能的