简介
Java Servlet规范2.3中引入了一个新的组件类型filter,filter可以动态拦截用户请求(request)和服务器回应(response), 开发者可以对这些数据进行想要的一些操作(例如修改其内容或者获取一些想要的信息)。过滤器一般情况下不会自己生成回应(response),但是它会提供一些通用的可以被加到任意类型servlet和JSP页面中去的功能(关于这些通用功能,后面会详细讲解)。
过滤器很多情况下都很有用:
一方面,过滤器可以将反复出现的任务封装成为一个可重复利用的模块。有条理的开发者们会持续关注哪些可以模块化他们代码的方法,模块化代码会更方便管理和文档化,可以更容易进行debug,如果写的好的话,还有利于移植。
另外一方面,过滤器可以用于对servlet或者JSP页面的回应(response)进行一些转换操作,一个常见的操作是对返回的数据进行格式化,客户端需要越来越多种类的格式化数据(例如WML)而不仅仅是HTML。为了适应这些客户端,需要一个强大的进行数据转换和过滤的组件。启事很多servlet和JSP页面的容器都提供了自己相应版本的过滤机制,这对开发者很有好处,但是各个容器的实现都不一样,导致代码不利于移植,所以,JSP规范对此做出了标准化的规定,这样,开发者写出来的过滤模块就可以被各个容器兼容了。
过滤器可以执行很多不同种类的功能,我们将要讨论下面以斜体字标记的部分(译者注:我将斜体部分标记为了红色):
- 认证: 基于用户身份的客户端请求阻塞
- 日志与审计:跟踪web应用的用户轨迹
- 图像转换: 按比例缩放地图等
- 数据压缩: 让下载的文件变得更小
- 本地化: 将请求和回应定位到特定的地点
- 对XML数据进行XSLT转换:将web应用的应答(response)定位到不同种类的客户端
这只是过滤器应用中的很少一部分,还有其它的很多用途,包括加密,加标记,触发资源访问事件,mime类型连锁化,和缓存等。
在这篇文章中,我们将首先讨论如何编程来实现如下类型的任务:
- 发出请求并执行对应的操作
- 阻止请求和回应向后继续传递
- 修改请求的头部和数据,用一个定制版本的请求来替换。
- 修改回应的头部和数据,用一个定制版本的回应来替换。
我们将简述过滤器的API,并且讨论如何去开发定制化的请求和回应。
编写过滤器代码只是完成了过滤器功能的一半,你还需要配置当应用部署到web容器上去的时候,过滤器如何映射到对应的Servlet。将具体的编程与配置解耦是过滤器机制的主要好处之一。
- 为了修改web应用的输入与输出你不需要重新编译任何内容。你只需要使用记事本程序或者其它的工具修改配置文件即可。例如,对一个PDF文件下载进行压缩只需要将一个压缩过滤器映射到对应的下载Servlet即可。
- 你可以毫不费力的用过滤器做一些实验,因为它们非常容易配置。
本文的最后一部分展示如何使用这种灵活的过滤器配置机制,一旦你阅读并理解了这篇文章,你就可以实现自己的过滤器而且有很多常见过滤器使用的窍门。
编写过滤器
过滤器的API定义在javax.servlet包中的Filter,FilterChain和FilterConfig三个接口(interface)中,定义过滤器需要实现Filter接口。filterchain对象(过滤器链)会被web容器传递给过滤器,它提供了调用一系列过滤器(而不仅仅是一个)的机制,一个filterconfig对象中包含初始化数据。
Filter接口中最重要的方法是doFilter,它是过滤器的核心,这个方法一般执行如下动作:
- 检查请求头部信息
- 修改请求(request)对象:如果它想要修改请求头部或数据或者将请求整个屏蔽掉的时候。
- 修改回应(response)对象:如果它想要修改回应头部或者数据的时候。
- 调用过滤器链(filter chain)中的下一个对象,如果当前filter是过滤器链(filter chain)中最后的一个,那么会调用目标资源,否则,会调用下一个在WAR中配置的过滤器,它通过调用filterchain对象的doFitler方法来达到这个目的(并且将请求对象(request)和回应对象(reponse)或者他们的包装类这两个参数传给doFilter方法)。它可以选择不调用filterchain的doFilter方法,这样请求实际上就被屏蔽掉了,在这种情况下,回应(reponse)对象需要过滤器来进行封装。
- 调用完filterchain的doFilter方法之后检查回应(response)头部
- 处理过程中出现错误跑出异常
除了doFilter方法之外,你还需要实现init和destroy方法,init方法会在过滤器初始化的时候被web容器调用,如果你需要向过滤器中传递初始参数,你可以在init方法中的FilterConfig对象中获取到这些参数。
例子:记录Servlet访问日志
现在你知道过滤器API的主要元素了,现在要我们看一个很简单的例子这个例子不会阻止请求,转换回应,或者其它很牛的功能,但是它对于开始学习过滤器API使用是很好的。
想象一个网站记录用户数的网站,如果想为一个已经存在的web应用增加这个功能,并且不想修改任何Servlet,你可以使用记录日志的过滤器。
HitCounterFilter在一个指定的Servlet被访问的时候会增加并记录一个计数器的值,在doFilter方法中,HitCounterFilter会首先从filter configuration对象中获取servlet context对象,这样,它就可以获取计数器对象,因为计数器被设置为context范围的对象,过滤器获取,增加并将计数器的值写入日志之后,它调用了filter chain对象的doFilter方法,下面是部分代码:
public final class HitCounterFilter implements Filter {
private FilterConfig filterConfig = null;
public void init(FilterConfig filterConfig)
throws ServletException {
this.filterConfig = filterConfig;
}
public void destroy() {
this.filterConfig = null;
}
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (filterConfig == null)
return;
StringWriter sw = new StringWriter();
PrintWriter writer = new PrintWriter(sw);
Counter counter = (Counter)filterConfig.
getServletContext().
getAttribute("hitCounter");
writer.println();
writer.println("===============");
writer.println("The number of hits is: " +
counter.incCounter());
writer.println("===============");
// Log the resulting string
writer.flush();
filterConfig.getServletContext().
log(sw.getBuffer().toString());
...
chain.doFilter(request, wrapper);
...
}
}
例子:修改请求(request)编码:
目前,很多浏览器不会在其HTTP请求头部的Content-Type中携带有关编码的相关信息,这个时候,web容器会使用默认的编码去解析请求中的参数,如果请求参数的编码与web容器的默认编码不同这个时候就会出现问题,你可以使用ServletRequest接口中的setCharacterEncoding方法设置编码。因为这个方法必须在或缺任何数据和参数内容之前被调用,所以它很适合使用过滤器来实现(是过滤器主要的使用方式之一)。
这个例子在Apache Tomcat 4.0版的样例代码中也有,过滤器根据初始化参数中指定的编码来设置请求编码。这个过滤器也可以恩容易的扩展用来根据发送过来的请求的特点进行设置编码,例如根据Accept-Language的值和User-Agent的内容,或者是存储在当前用户session中的某个值。
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain) throws
IOException, ServletException {
String encoding = selectEncoding(request);
if (encoding != null)
request.setCharacterEncoding(encoding);
chain.doFilter(request, response);
}
public void init(FilterConfig filterConfig) throws
ServletException {
this.filterConfig = filterConfig;
this.encoding = filterConfig.getInitParameter("encoding");
}
protected String selectEncoding(ServletRequest request) {
return (this.encoding);
}
编程定制请求(request)和回应(response)
到此为止我们已经学习了几个简单的例子,现在来看一个稍微复杂些的例子,这个过滤器用来修改客户端的请求或者发给客户端的回应,一个过滤器有很多修改请求或者回应的方法,例如,过滤器可以向请求中增加一个属性、向回应中插入或者修改数据数据。
一个用来修改回应(response)的过滤器必须在这个回应发回给客户端之前捕获它,方法是给生成回应的Servlet传递一个替代流(stand-in stream),这个替代流阻止Servlet管理原始的回应流(origianl response stream),这样过滤器就可以修改Servlet的回应了。
为了将这个替代流传递给Servlet,过滤器创建一个回应的包装器,覆盖getWriter或者getOutputStream方法来返回这个替代流。这个包装器被传递给filterchain对象的doFilter方法,包装器中的方法最终调用的还是原始请求(request)和应答(response)中的方法。这个方法遵循著名的《设计模式:可复用面向对象软件的基础》一书中所讲的包装器和装饰器模式。紧接着的部分我们会讲之前提到的访问计数器(hit counter)以及其它类型的过滤器是如何使用包装器的。
为了覆盖请求(request)中的方法,你需要包装一个继承了ServletRequestWrapper或者HttpServletRequestWrapper类的对象。为了覆盖应答(reponse)方法,你需要包装一个继承了ServletRTesponseWrapper或者HpptServletResponseWrapper类的对象。
在前面“编写过滤器”这一节提到的访问计数器过滤器将计数器的值插入到应答中,前面的HitCounterFilter中被省略了的代码如下:
PrintWriter out = response.getWriter();
CharResponseWrapper wrapper = new CharResponseWrapper(
(HttpServletResponse)response);
chain.doFilter(request, wrapper);
if(wrapper.getContentType().equals("text/html")) {
CharArrayWriter caw = new CharArrayWriter();
caw.write(wrapper.toString().substring(0,
wrapper.toString().indexOf("</body>")-1));
caw.write("<p>\nYou are visitor number
<font color='red'>" + counter.getCounter() + "</font>");
caw.write("\n</body></html>");
response.setContentLength(caw.toString().length());
out.write(caw.toString());
} else {
out.write(wrapper.toString());
}
out.close();
HitCounterFilter用CharResponseWrapper包装了应答(response)。CharResponseWrapper覆盖了getWriter方法来返回一个替代流(stand-in stream),这样过滤器链(filter chain)最末端的Servlet可以向这个替代流中写应答(response)。
当chain.doFilter调用返回的时候,HitCounterFilter从PrinterWriter中搜索servlet的应答,发现它如果是HTML的话则将其写入到一个缓冲区(buffer)。过滤器将计数器的值写入缓冲区,将应答头部中内容的长度重置,最后将缓冲区内同写入到应答流中。
public class CharResponseWrapper extends
HttpServletResponseWrapper {
private CharArrayWriter output;
public String toString() {
return output.toString();
}
public CharResponseWrapper(HttpServletResponse response){
super(response);
output = new CharArrayWriter();
}
public PrintWriter getWriter(){
return new PrintWriter(output);
}
}
样例:压缩应答(Conpressing the Response)
另外一个使用过滤器来修改应答的例子就是包含在Tomcat servlet引擎样例代码中包含的压缩过滤器。虽然高速因特网连接现在变得越来越普遍,仍然由必要有效的利用带宽。一个压缩过滤器是非常方便的,因为你可以讲这个过滤器附加到任意servlet上来减少应答的大小。
就像访问计数器过滤器(hit counter filter)一样,压缩过滤器也创建了一个替代流(在本例中是CompressionResponseStream)用来让servlet将应答写进去和包装传递给Servlet的应答。
只要客户端可以接收一个压缩后的应答那么过滤器就可以创建包装器和替代流。Servlet将应答写入从包装器接收到的压缩流(compression stream)。CompressionResponseStream覆盖了write方法,一旦应答数据大小超过了某个阈值,就会调用这个方法将应答数据写入一个GZIPOutputStream对象。
public void write(int b) throws IOException {
...
if ((bufferCount >= buffer.length) ||
(count>=compressionThreshold)) {
compressionThresholdReached = true;
}
if (compressionThresholdReached) {
writeToGZip(b);
} else {
buffer[bufferCount++] = (byte) b;
count++;
}
}
样例:改变应答数据
最后一个我们要讨论的过滤器是一个XSLT过滤器,XSLT是一种用来转换XML数据的语言。亦可以使用XSLT将一个XML文件转换为终端用户指定的格式,例如HTML或PDF,或者是另外一种XML格式,一些应用的例子包括:
- 将一个公司要求格式的XML文件转换为另外一个公司要求的XML格式
- 根据用户的喜好定制web页面的外观
- 使一个web应用能够向不同种类的客户端做出应答,例如,根据查看User-Agent头部的信息,来判定对方是WML电话还是cHTML电话 ,并选择不同的样式
设想有这么一个web服务,根据客户请求返回产品清单,下面的XML文件是应答的样例:
<book>
<isbn>123</isbn>
<title>Web Servers for Fun and Profit</title>
<quantity>10</quantity>
<price>$17.95</price>
</book>
下面的XSL样式将上面的XML文件渲染成为符合用户要求的HTM格式的清单或者是符合特定机器要求的XML格式。
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="book"> <html>
<body>There are <xsl:value-of select="quantity"/> copies of
<i><xsl:value-of select="title"/></i> available.
</body>
</html>
</xsl:template>
</xsl:stylesheet>
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="no"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="book">
<xsl:element name="book">
<xsl:attribute name="isbn"><xsl:value-of select="isbn"/></
xsl:attribute>
<xsl:element name="quantity"><xsl:value-of select="quantity"/
></xsl:element>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
下面的XSLT过滤器根据请求中包含的参数的值使用这和样式将应答转换为不同的格式。过滤器根据请求参数的值来设置应答中的内容类型(content type)。应答被一个CharResponseWrapper包装之后传递给了过滤器链(filter chain)的doFilter方法。过滤器链中的最后一个元素是一个用来返回前述的清单应答的servlet。当doFilter返回的时候,过滤器从包装器中检索应答数据并使用样式进行转换。
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String contentType;
String styleSheet;
String type = request.getParameter("type");
if (type == null || type.equals("")) {
contentType = "text/html";
styleSheet = "/xml/html.xsl";
} else {
if (type.equals("xml")) {
contentType = "text/plain";
styleSheet = "/xml/xml.xsl";
} else {
contentType = "text/html";
styleSheet = "/xml/html.xsl";
}
}
response.setContentType(contentType);
String stylepath=filterConfig.getServletContext().
getRealPath(styleSheet);
Source styleSource = new StreamSource(stylePath);
PrintWriter out = response.getWriter();
CharResponseWrapper responseWrapper =
new CharResponseWrapper(
(HttpServletResponse)response);
chain.doFilter(request, wrapper);
// Get response from servlet
StringReader sr = new StringReader(
new String(wrapper.getData()));
Source xmlSource = new StreamSource((Reader)sr);
try {
TransformerFactory transformerFactory =
TransformerFactory.newInstance();
Transformer transformer = transformerFactory.
newTransformer(styleSource);
CharArrayWriter caw = new CharArrayWriter();
StreamResult result = new StreamResult(caw);
transformer.transform(xmlSource, result);
response.setContentLength(caw.toString().length());
out.write(caw.toString());
} catch(Exception ex) {
out.println(ex.toString());
out.write(wrapper.toString());
}
}
指定过滤器配置
既然我们已经知道了如何编写一个过滤器程序,最后一步就是学习将其应用到一个web组件或者一组web组件。为了将一个过滤器映射到一个servlet,你需要:
- 在web应用部署描述文件中,使用<filter>元素声明一个过滤器。这个元素为过滤器创建一个名字并声明过滤器的时候类和初始化参数。
- 使用部署描述文件中的<filter-mapping>元素将过滤器映射到一个servlet。这个元素使用名字或者URL模式来将一个过滤器映射到一个servlet。
下面展示了如何指定前面提到的压缩过滤器需要的元素。为了定义压缩过滤器,你需要给过滤器提供一个名字,说明实现过滤器需要的类,还有阈值初始化参数的名字和值。
<filter>
<filter-name>Compression Filter</filter-name>
<filter-class>CompressionFilter</filter-class>
<init-param>
<param-name>compressionThreshold</param-name>
<param-value>10</param-value>
</init-param>
</filter>
filter-mapping元素将压缩过滤器映射到哦啊conpresionTest这个servlet。映射中也指定了URL模式为/compressionTest。需要注意的是filter,filter-mapping, servlet和servlet-mapping这几个元素需要按顺序依次出现。
<filter-mapping>
<filter-name>Compression Filter</filter-name>
<servlet-name>CompressionTest</servlet-name>
</filter-mapping>
<servlet>
<servlet-name>CompressionTest</servlet-name>
<servlet-class>CompressionTest</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CompressionTest</servlet-name>
<url-pattern>/CompressionTest</url-pattern>
</servlet-mapping>
需要注意的是,这个映射会让所有的发给ConpressionTest的请求或者任何映射到/CompressionTest URL的servlet JSP或者静态内容都会导致这个过滤器被调用。
如果你想为每次对某个web应用的访问都记录日志的话,你需要将访问计数器映射到 /* 这个URL模式,下面是部署文件样例:
?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web
Application 2.3//EN" "http://bit.ly/15eRp2z">
<web-app>
<filter>
<filter-name>XSLTFilter</filter-name>
<filter-class>XSLTFilter</filter-class>
</filter>
<filter>
<filter-name>HitCounterFilter</filter-name>
<filter-class>HitCounterFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HitCounterFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>XSLTFilter</filter-name>
<servlet-name>FilteredFileServlet</servlet-name>
</filter-mapping>
<servlet>
<servlet-name>FilteredFileServlet</servlet-name>
<servlet-class>FileServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>FilteredFileServlet</servlet-name>
<url-pattern>/ffs</url-pattern>
</servlet-mapping>
</web-app>
就像你所看到的,你可以将一个过滤器映射到一个或者多个servlet上,你也可以将多个过滤器映射到一个servlet上。
回想一个过滤器链是传递给过滤器的doFilter方法的参数对象之一。这个链是根据过滤器映射形成的,过滤器在这个链中的顺序就是他们在web应用部署文件的过滤器映射中出现的顺序
当一个URL被映射到了servlet S1,web容器会调用过滤器F1的doFilter方法,S1对应的过滤器链中的每个过滤器的doFilter方法都会通过前一个过滤器的chain.doFilter语句被调用。因为S1的过滤器链包括F1和F3。F1中对chain.doFilter的调用会导致F3中doFilter的调用。当F3的doFilter执行完之后,控制流会返回F1的doFilter方法。
部署描述文件将访问计数器过滤器和XSLT过滤器放在FilteredFileSrvlet的过滤器链中。当FilteredFileServlet被调用的时候,访问计数器过滤器会记录日志,但是只有当应答的类型是HTML的时候才会在XSLT转换之后将计数器的值插入到应答数据中。
结论
过滤器机制提供了一种将某个组件中可以在多种情形下通用的功能封装起来的方法。过滤器很容易编写和配置,也很容易移植和重用。总之,过滤器是一个web开发者工具箱中的核心元素之一。
致谢
略
资源
你可以通过下载Tomcat 4.0来获取字符编码过滤器和压缩过滤器。字符编码过滤器位于TOMCAT_HOME/webapps/examples/WEB-INF/classes/filters目录下。压缩过滤器在TOMCAT_HOME/webapps/examples/WEB-INF/classes/compressionFilters目录下。