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
方法也会再执行两次,日志文件中也会多出两条记录,如下图所示: