10.3 示例:AutoCorrectFilter
10.3示例:AutoCorrectFilter
在Web应用中,用户经常在单词的前面或者后面输入空格,更有甚者在单词之间也加入空格。是否很想在应用的每个Servlet中,把多余的空格删除掉呢?本节的AutoCorrectFilter可以帮助你搞定它。该Filter包含了HttpServletRequestWrapper子类AutoCorrectHttpServletRequestWrapper,并重写了返回参数值的方法:getParameter、getParameterValues及getParameterMap。
AutoCorrectFilter.java
1 | package filter; |
代码详解
doFilter方法
这个Filter的doFilter方法非常简单:创建ServletRequest的修饰类实例,然后,把修饰类实例传给doFilter:
1 | //获取被修饰类的实例 |
AutoCorrectHttpServletRequestWrapper.java
AutoCorrectHttpServletRequestWrapper.java实现了对HttpServletRequest对象的装饰.如下所示:
1 | package filter; |
修饰方法
在这个Filter背后的任何Servlet获得的HttpServletRequest都将被AutoCorrectHttpServletRequestWrapper所封装。这个封装类很长,但很好理解。简单地说,就是它把所有获取参数方法的返回值都用autoCorrect方法先处理删除其中的空格,然后使用autoCorrect方法处理后的结果:
1 | private String autoCorrect(String value) { |
测试
测试这个Filter时,可以分别下面的test1.jsp,test2.jsp这两个页面。
test1.jsp页面
1 | <!DOCTYPE HTML> |
test2.jsp页面
1 | <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> |
运行效果
可以使用如下URL路径访问test1.jsp页面:
http://localhost:8080/app10a/test1.jsp
输入一个带空格的单词,无论是前面、后面,还是在单词之间,然后点击提交。接下来,在显示器上你将看到这些输入单词都被修正过来。如下图所示:

10.2 Servlet装饰类
10.2 Servlet装饰类
Servlet装饰类源自于4个实现类,它很少被使用,但是十分强大:ServletRequestWrapper、ServletResponseWrapper以及HttpServletRequestWrapper、HttpServletResponseWrapper。
这几个装饰类非常便于使用,因为它提供了每个方法的默认实现:
即ServletRequest封闭的配置方法。通过继承ServletRequestWrapper,只需要实现你需要变更的方法就可以了。如果不用ServletRequestWrapper,则需要继承ServletRequest并实现ServletRequest中所有的方法。
装饰器设计模式中ServletRequestWrapper的类图如下图所示。Servlet容器在每次Servlet服务调用时创建ServletRequest、ContainerImpl。直接继承ServletRequestWrapper就可以装饰ServletRequest了。
10.1 装饰器(Decorator)模式
10.1 装饰器模式
装饰器模式
装饰器模式,顾名思义,就是对已经存在的某些类进行装饰,以此来扩展一些功能
即使你没有该对象的源代码或者该对象标识为final。Decorator模式适用于无法继承该类(例如,对象的实现类使用final标识)或者无法创建该类的实例,但从另外的系统中可以取得该类的实现的情况。例如,Servlet容器方法。只有一种方法可以修改ServletRequest或者ServletResponse行为,即在另外的对象中封装该实例。
唯一的限制是,修饰对象必须继承一个接口,然后实现接口以封装这些方法。
Decorator模式的URL类图
UML类图如下图所示。
图中的类图说明了一个Component接口以及它的实现类ComponentImpl。Component接口定义了名为A的方法。为了修饰ComponentImpl的实例,需要创建一个Decorator类,并实现Component接口,然后在子类中扩展Decorator的新行为。在类图中DecoratorA就是Decorator的一个子类。每个Decorator实例需要包含Component的一个实例。Decorator类代码如下(注意在构造函数中获取了Component的实例,这意味着创建Decorator对象只能传入Component的实例):
实例
Decorator.java
1 | //实现Component,以修饰Component |
在Decorator类中,有修饰的方法就是可能在子类中需要修改行为的方法,在子类中不需要修饰的方法可以不需要实现。所有的方法,无论是否需要修饰,都叫作Component中的配对方法。Decorator是一个非常简单的类,便于提供每个方法的默认实现。修改行为在它的子类中。
需要牢记一点,**Decorator类及被修饰对象的类需要实现相同的接口。为了实现Decorator,可以在Decorator中封装修饰对象,并把Decorator作为Component的一个实现**。任何Component的实现都可以在Decorator中注入。事实上,你可以把一个修饰的对象传入另一个修饰的对象,以实现双重的修饰。
参考资料
第10章修饰Requests及Responses概述
第10章修饰Requests及Responses概述
Servlet API包含4个可修饰的类,用于改变ServletRequest以及Servlet Response。这种修饰允许修改ServletRequest以及ServletResponse或者HTTP中的等价类(即HttpServletRequest和HttpServletResponse)中的任务方法。这种修饰遵循Decorator模式或者Wrapper模式,因此在使用修饰前,需要了解一下该模式的内容。
本章从解释Decorator模式开始,说明如何通过修饰HttpServletRequest来修改HttpServletRequest对象的行为。该技术同样适用于修饰HttpServletResponse对象。
9.7 小结
9.6 Filter的触发顺序
9.6 Filter的触发顺序
**如果多个Filter应用于同一个资源,Filter的触发顺序将变得非常重要,这时就需要使用部署描述来管理Filter**:指定哪个Filter先被触发。例如:Filter 1需要在Filter 2前被触发,那么在部署描述中,Filter 1需要配置在Filter 2之前:
1 | <filter> |
通过部署描述之外的配置来指定Filter触发的顺序是不可能的。第13章将会有更多部署描述的说明。
9.5 示例3:下载计数Filter
9.5 示例3:下载计数Filter
本例子中,下载计数Filter将会示范如何在Filter中计算资源下载的次数。这个示例特别有用,它将会得到文档、音频文件的受欢迎程度。作为简单的示例,这里将数值保存在属性文件中,而不保存在数据库中。其中资源的ULR路径将作为属性名保存在属性文件中。
因为我们把值保存在属性文件中,并且**Filter可以被多线程访问,因此涉及线程安全问题。用户访问一个资源时,Filter需要读取相应的属性值加1,然后保存该值。如果第二个用户在第一个线程完成前同时访问该资源,将会发生什么呢?计算值出错。在本例中,读写的同步锁并不是一个好的解决这个问题的方法,因为它会导致扩展性问题。
本示例中,解决这个线程安全问题是通过Queue以及Executor。如果不熟悉这两个Java类型的话,请看第18章”多线程及线程安全”
简而言之,进来的Request请求将会保存在单线程Executor的队列中**。替换这个任务十分方便,因为这是一个异步的方法,因此你不需要等待该任务结束。Executor一次从队列中获取一个对象,然后做相应属性值的增加。由于Executor只在一个线程中使用,因此可以消除多个线程同时访问一个属性文件的影响。
DownloadCounterFilter.java
1 | package filter; |
代码详解
init方法
如果在当前应用的工作目录中不存在downloadLog.txt文件,这个Filter的init方法就会创建它:
1 | //获取项目所在的根目录 |
接着创建Properties对象,并读取该文件:
1 | //创建配置文件 |
注意,Filter的实现类中引用到了ExecutorService(Executor的子类):
1 | ExecutorService executorService = |
destroy方法
且当Filter销毁时,会调用ExecutorService的shutdown方法:
1 | public void destroy() { |
doFilter方法
Filter的doFilter实现中大量地使用到这个Job。每次URL请求都会调用到ExecutorService的execute方法,然后才调用FilterChaing.doFilter()。该任务的execute实现非常好理解:它将URL作为一个属性名,从Properties实例中获取该属性的值,然后加1,并调用flush方法写回到指定的日志文件中:
1 | public void run() { |
这个Filter的urlPatterns设置为*,即过滤所有URL,实际应用中可以修改为过滤某些特定的文件,如:只过滤pdf文件
运行效果
访问项目中的多个jsp页面,然后多刷新几次,然后打开打开downloadLog.txt文件(在Tomcat中的路径E:\apache-tomcat-8.5.35\webapps\app09a\downloadLog.txt)可以看到效果,如下图所示:
9.4 示例2:图像文件保护Filter
9.4 示例2:图像文件保护Filter
项目描述
本例中的图像文件保护Filter用于防止用户在浏览器地址栏中直接输入图像的URL路径来下载图像。应用中只显示通过<img/>标签链接的图片
原理
该Filter的实现原理是检查HTTP Header的referer值。如果该值为null,就意味着当前的请求中没有referer值,即当前的请求是直接通过输入URL来访问该资源的。如果资源的Header值为非空,将返回Request语法的原始页面作为referer值。
ImageProtectorFilter.java
Filter实现类ImageProtectorFilter.java如下所示,从WebFilter的Annotation中,可以看到该Filter应用于所有的.png、.jpg、.gif文件后缀。
1 | package filter; |
代码详解
这里并没有init和destroy方法。其中doFilter方法读取到Header中的referer值,要确认是要继续处理这个资源还是给个异常:
1 | String referrer = httpServletRequest.getHeader("referer"); |
image.jsp
1 | avatar.png:<br> |
图片资源
在WebContent目录下创建image目录,在image目录中粘贴如下图片(avatar.png):![]()
项目结果如下图所示:
运行效果
浏览器地址栏中输入地址访问图片
测试该Filter,可以在浏览器地址栏中输入如下ULR路径,尝试访问logo.png图像:
1 | http://localhost:8080/app09a/image/avatar.png |
将会得到“Image not available”的错误提示。如下图所示:
jsp页面中图片的显示效果
接下来,通过image.jsp的页面来访问该图像:
http://localhost:8080/app09a/image.jsp
可以访问到该图像了,如下图所示:
这种方法生效的原因是image.jsp页面中包含了通知浏览器下载图像的链接:
1 | <img alt='avatar.png' src='image/avatar.png' /> |
当浏览器通过该链接获取图像资源时,它也将该页面的URL(本示例中为http://localhost:8080/app09a/image/avatar.png)作为Header的referer值传到服务中。可以在浏览器中按下F12查看该图片的请求头中的Referer信息,如下图所示:
9.3示例1:日志Filter
9.3示例1:日志Filter
实例简介
作为第1个例子,将做一个简单的Filter:
- 在
app09a的应用中把Request请求的URL记录到日志文本文件中。 - 日志文本文件名通过
Filter的初始化参数来配置。- 此外,日志的每条记录都会有一个前缀,该前缀也由
Filter初始化参数来定义。
- 此外,日志的每条记录都会有一个前缀,该前缀也由
通过日志文件,可以获得许多有用的信息,例如在应用中哪些资源访问最频繁;Web站点在一天中的哪个时间段访问量最多。
这个Filter的类名叫LoggingFilter,一般情况下,Filter的类名都以*Filter结尾。
LoggingFilter类
1 | package filter; |
下面来仔细分析一下该Filter类。
成员变量
首先,该Filter的类实现了Filter的接口并声明两个成员变量:PrintWriter类型的logger和String类型的prefix。
1 | private PrintWriter logger; |
其中PrintWriter用于记录日志到文本文件,prefix的字符串用于每条日志的前缀。
WebFilter注解
Filter的类使用了@WebFilter的Annotation,将两个参数(logFilteName、prefix)传入到该Filter中:
1 | @WebFilter( |
init方法
获取初始化参数
在Filter的init方法中,通过形式参数FilterConfig对象的getInitParameter方法来获取prefix和getFileName的初始化参数。其中把prefix参数中赋给了类变量prefix,logFileName则用于创建一个PrintWriter:
1 | prefix = filterConfig.getInitParameter("prefix"); |
工作目录
如果Servlet/JSP应用是通过Servlet/JSP容器启动的,那么当前应用的工作目录是当前JDK所在的目录。
如果是在Tomcat中,该目录是Tomcat的安装目录。
在应用中创建日志文件,可以通过ServletContext.getRealPath来获取工作目录,结合应用工作目录以及初始化参数中的logFilterNmae,就可以得到日志文件的绝对路径:
1 | String appPath = |
当Filter的init方法被执行时,日志文件就会创建出来。如果在应用的工作目录中该文件已经存在,那么该日志文件的内容将会被覆盖。
destroy方法
当应用关闭时,PrintWriter需要被关闭。因此在Filter的destroy方法中,需要:
1 | if (logger != null) { |
doFilter方法
Filter的doFilter实现中记录着所有从ServletRequest到HttpServletRequest的Request,并调用了它的getRequestURI方法,该方法的返回值将记录通过PrintWriter的println方法记录下来:
1 | HttpServletRequest httpServletRequest = |
每条记录都有一个时间戳以及前缀,这样可以很方便地标识每条记录。接下来 Filter的doFilter实现调用PrintWriter的flush方法以及FilterChain.doFilter,以唤起资源的调用:
1 | logger.flush(); |
如果使用Tomcat,Filter的初始化并不会等到第一个Request请求时才触发进行。这点可以在控制台中打印出来的logFileName参数值中可以看到。
test.jsp
1 | <%@ page language="java" contentType="text/html; charset=UTF-8" |
运行效果
启动项目
启动app09a项目,可以看到控制台部分输出如下图所示:
从控制台的输出我们可以知道:
- 日志文件的绝对路径为:
E:\apache-tomcat-8.5.35\webapps\app09a\log.txt. - 在项目启动阶段
Filter初始化就开始了。
访问JSP页面
然后通过URL调用test.jsp页面
http://localhost:8080/app09a/test.jsp
此时,容器会调用doFilter方法,该方法控制台输出如下:
查看日志文件
这时候可以打开日志文件:E:\apache-tomcat-8.5.35\webapps\app09a\log.txt查看日志输出,如下图所示:
doFilter的执行次数和JSP页面的访问次数的关系
在该过滤器中,我们设置的过滤规则为:
1 | urlPatterns ={"/*"} |
这表示过滤所有的JSP页面,所以每访问一次JSP页面,过滤器就会执行一次.
再刷新两次test.jsp页面,则doFilter方法也会再执行两次,日志文件中也会多出两条记录,如下图所示:
