Servlet过滤器介绍之实用过滤器
这里介绍几个实用的过滤器设计代码。整理自Marty Halls写的《ServletJSP权威指南》。我已做过测试,均通过可用。

author: ZJ 2007-3-5
6.禁止站点过滤器

如果你希望在你的过滤器检测到不正常的异常而中途中断后面的过滤过程时,可这样做:

public void doFilter(ServletRequest request, ServletResponse response,

       FilterChain chain) throws ServletException, IOException {

    HttpServletRequest req = (HttpServletRequest) request;

    HttpServletResponse res = (HttpServletResponse) response;

    if (isUnusualCondition(req)) {

       res.sendRedirect("http://www.somesite.com");

    } else {

       chain.doFilter(req, res);

    }

}
下例是一个禁止站点过滤器,如果不希望某些站点访问你的网站,你可以在web.xmlparam-value中列出它的站点,然后应用上面的原理跳出常规过滤,给出禁止访问的页面。

BannedAccessFilter.java

package com.zj.sample;

import java.io.IOException;

import java.io.PrintWriter;

import java.net.MalformedURLException;

import java.net.URL;

import java.util.HashSet;

import java.util.StringTokenizer;

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.http.HttpServletRequest;

 

public class BannedAccessFilter implements Filter {

    private HashSet<String> bannedSiteTable;

 

/**

* Deny access if the request comes from a banned site or is referred here

* by a banned site.

 */

    public void doFilter(ServletRequest request, ServletResponse response,

           FilterChain chain) throws ServletException, IOException {

       System.out.println("Within BannedAccessFilter:Filtering the Request...");

       HttpServletRequest req = (HttpServletRequest) request;

       String requestingHost = req.getRemoteHost();

       String referringHost = getReferringHost(req.getHeader("Referer"));

       String bannedSite = null;

       boolean isBanned = false;

       if (bannedSiteTable.contains(requestingHost)) {

           bannedSite = requestingHost;

           isBanned = true;

       } else if (bannedSiteTable.contains(referringHost)) {

           bannedSite = referringHost;

           isBanned = true;

       }

       if (isBanned) {

           showWarning(response, bannedSite);

       } else {

           chain.doFilter(request, response);

       }

       System.out.println("Within BannedAccessFilter:Filtering the Response...");

    }

 

/**

* Create a table of banned sites based on initialization parameters.

* Remember that version 2.3 of the servlet API mandates the use of the

* Java  2 Platform. Thus, it is safe to use HashSet (which determines

* whether a given key exists) rather than the clumsier Hashtable

* (which has a value for each key).

*/

 

    public void init(FilterConfig config) throws ServletException {

       bannedSiteTable = new HashSet<String>();

       String bannedSites = config.getInitParameter("bannedSites");

       // Default token set: white space.

       StringTokenizer tok = new StringTokenizer(bannedSites);

       while (tok.hasMoreTokens()) {

           String bannedSite = tok.nextToken();

           bannedSiteTable.add(bannedSite);

           System.out.println("Banned " + bannedSite);

       }

    }

 

    public void destroy() {}

 

    private String getReferringHost(String refererringURLString) {

       try {

           URL referringURL = new URL(refererringURLString);

           return (referringURL.getHost());

       } catch (MalformedURLException mue) { // Malformed or null

           return (null);

       }

    }

 

    // Replacement response that is returned to users

    // who are from or referred here by a banned site.

    private void showWarning(ServletResponse response, String bannedSite)

           throws ServletException, IOException {

       response.setContentType("text/html");

       PrintWriter out = response.getWriter();

       String docType = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 "

              + "Transitional//EN\">\n";

       out.println(docType + "<HTML>\n"

              + "<HEAD><TITLE>Access Prohibited</TITLE></HEAD>\n"

              + "<BODY BGCOLOR=\"WHITE\">\n" + "<H1>Access Prohibited</H1>\n"

              + "Sorry, access from or via " + bannedSite + "\n"

              + "is not allowed.\n" + "</BODY></HTML>");

    }

}

 

web.xml

<filter>

    <filter-name>BannedAccessFilter</filter-name>

    <filter-class>com.zj.sample.BannedAccessFilter</filter-class>

    <init-param>

       <param-name>bannedSites</param-name>

       <param-value>

           [url]www.competingsite.com[/url] [url]www.bettersite.com[/url]

           [url]www.moreservlets.com[/url] 127.0.0.1//我们测试这个

       </param-value>

    </init-param>

</filter>

<filter-mapping>

    <filter-name>BannedAccessFilter</filter-name>

    <url-pattern>/*</url-pattern>

</filter-mapping>

 

测试:

[url]http://localhost:8080/Test4Jsp/[/url]

 

结果:


7.替换过滤器

7.1修改响应

过滤器能够阻止对资源的访问或者阻止激活它们。但如果过滤器想更改资源所生成的响应。怎么办呢?似乎没有办法能够对一个资源所生成的响应进行访问。DoFilter的第二个参数(ServletResponse)给过滤器提供了一种发送新输出到客户机的办法,但没有给过滤器提供对servletJSP页面输出进行访问的办法。为什么会这样呢?因为在第一次调用doFilter方法时,servletJSP页面甚至还没有执行。一旦调用了FilterChain对象中的doFilter方法,要修改响应似乎就太迟了,这是数据已经发送到客户机。
不过,办法是有的,那就是修改传递给FilterChain对象的doFilter方法的响应对象。一般,建立缓存servletJSP页面生成的所有输出的版本。Servlet API 2.3版为此提供了一种有用的资源,即,HttpServletResponseWrapper类。这个类的使用包括以下五个步骤:
1)建立一个响应包装器。扩展javax.servlet.http.HttpServletResponseWrapper
2)提供一个缓存输出的PrintWriter。重载getWriter方法,返回一个保存发送给它的所有东西的PrintWriter,并把结果存进一个可以稍后访问的字段中。
3)传递该包装器给doFilter。此调用是合法的,因为HttpServletResponseWrapper实现HttpServletResponse
4)提取和修改输出。在调用FilterChaindoFilter方法后,原资源的输出只要利用步骤2中提供的机制就可以得到。只要对你的应用适合,就可以修改或替换它。
5)发送修改过的输出到客户机。因为原资源不再发送输出到客户机(这些输出已经存放到你的响应包装器中了),所以必须发送这些输出。这样,你的过滤器需要从原响应对象中获得PrintWriterOutputStream,并传递修改过的输出到该流中。

 

7.2一个可重用的响应包装器

下例程序给出了一个包装器,它可用于希望过滤器修改资源的输出的大多数应用中。CharArrayWrapper类重载getWriter方法以返回一个PrintWriter,它累积一个大字符数组中的所有东西。开发人员可利用toCharArray(原始char[])或toString(从char[]得出的一个String)方法得到这个结果。
CharArrayWrapper.java

package com.zj.sample;

import java.io.CharArrayWriter;

import java.io.PrintWriter;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpServletResponseWrapper;

 

/**

 * A response wrapper that takes everything the client would normally

 * output and saves it in one big character array.

 */

public class CharArrayWrapper extends HttpServletResponseWrapper {

    private CharArrayWriter charWriter;

 

    /**

     * Initializes wrapper.

     * <P>

     * First, this constructor calls the parent constructor. That call

     *is crucial so that the response is stored and thus setHeader, *setStatus, addCookie, and so forth work normally.

     * <P>

     * Second, this constructor creates a CharArrayWriter that will

*  be used to accumulate the response.

     */

    public CharArrayWrapper(HttpServletResponse response) {

       super(response);

       charWriter = new CharArrayWriter();

    }

 

    /**

     * When servlets or JSP pages ask for the Writer, don't give them

* the real one. Instead, give them a version that writes into

* the character array.

     * The filter needs to send the contents of the array to the

* client (perhaps after modifying it).

     */

    public PrintWriter getWriter() {

       return (new PrintWriter(charWriter));

    }

 

    /**

     * Get a String representation of the entire buffer.

     * <P>

     * Be sure <B>not</B> to call this method multiple times on the same

     * wrapper. The API for CharArrayWriter does not guarantee that it

     * "remembers" the previous value, so the call is likely to make

* a new String every time.

     */

    public String toString() {

       return (charWriter.toString());

    }

 

    /** Get the underlying character array. */

    public char[] toCharArray() {

       return (charWriter.toCharArray());

    }

}

 

7.3 替换过滤器

这里展示前一节中给出的CharArrayWrapper的一个常见的应用:更改一个多次出现的目标串为某个替代串的过滤器。

 

7.3.1通用替换过滤器
ReplaceFilter.java给出一个过滤器,它在CharArraryWrapper中包装响应,传递该包装器到FilterChain对象的doFilter方法中,提取一个给出所有资源的输出的String型值,用一个替代串替换某个目标串的所有出现,并发送此修改过的结果到客户机。
关于这个过滤器,有两件事情需要注意。首先,它是一个抽象类。要使用它,必须建立一个提供getTargetStringgetReplacementString方法的实现的子类。下一小节中给出了这种处理的一个例子。其次,它利用一个较小的实用类(见FilterUtils.java)来进行实际的串替换。你可使用新的常规表达式包而不是使用StringStringTokenizer中低级的和繁琐的方法。
ReplaceFilter.java

package com.zj.sample;

import java.io.IOException;

import java.io.PrintWriter;

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.http.HttpServletResponse;

 

/**

 * Filter that replaces all occurrences of a given string with a

* replacement.

 * This is an abstract class: you <I>must</I> override the getTargetString

* and getReplacementString methods in a subclass.

* The first of these methods specifies the string in the response

* that should be replaced. The second of these specifies the string

* that should replace each occurrence of the target string.

 */

public abstract class ReplaceFilter implements Filter {

    private FilterConfig config;

 

    public void doFilter(ServletRequest request, ServletResponse response,

           FilterChain chain) throws ServletException, IOException {

       CharArrayWrapper responseWrapper = new CharArrayWrapper(

              (HttpServletResponse) response);

       // Invoke resource, accumulating output in the wrapper.

       chain.doFilter(request, responseWrapper);

       // Turn entire output into one big String.

       String responseString = responseWrapper.toString();

       // In output, replace all occurrences of target string with replacement

       // string.

       responseString = FilterUtils.replace(responseString, getTargetString(),

              getReplacementString());

       // Update the Content-Length header.

       updateHeaders(response, responseString);

       PrintWriter out = response.getWriter();

       out.write(responseString);

    }

 

    /**

     * Store the FilterConfig object in case subclasses want it.

     */

    public void init(FilterConfig config) throws ServletException {

       this.config = config;

    }

 

    protected FilterConfig getFilterConfig() {

       return (config);

    }

 

    public void destroy() {

    }

 

    /**

     * The string that needs replacement.

*Override this method in your subclass.

     */

    public abstract String getTargetString();

 

    /**

     * The string that replaces the target. Override this method in

     * your  subclass.

     */

    public abstract String getReplacementString();

 

    /**

     * Updates the response headers. This simple version just sets

*the Content-Length header, assuming that we are using a

*character set that uses 1 byte per character.

* For other character sets, override this method to use

* different logic or to give up on persistent HTTP connections.

* In this latter case, have this method set the Connection header

* to "close".

     */

    public void updateHeaders(ServletResponse response, String responseString) {

       response.setContentLength(responseString.length());

    }

}

 

FilterUtils.java

package com.zj.sample;

 

/**

 * Small utility to assist with response wrappers that return strings.

 */

public class FilterUtils {

    /**

     * Change all occurrences of orig in mainString to replacement.

     */

    public static String replace(String mainString, String orig,

           String replacement) {

       String result = "";

       int oldIndex = 0;

       int index = 0;

       int origLength = orig.length();

       while ((index = mainString.indexOf(orig, oldIndex)) != -1) {

           result = result + mainString.substring(oldIndex, index)

                  + replacement;

           oldIndex = index + origLength;

       }

       result = result + mainString.substring(oldIndex);

       return (result);

    }

}

 

7.3.2实现一个字符替换过滤器
假设百度收购了google(只是假设),现在所有的页面上凡是出现google字样的文字都必须替换为百度!ReplaceSiteNameFilter.java继承上文ReplaceFilter.java来实现这一功能。
ReplaceSiteNameFilter.java

package com.zj.sample;

 

public class ReplaceSiteNameFilter extends ReplaceFilter {

    public String getTargetString() {

       return ("google.com.cn");

    }

 

    public String getReplacementString() {

       return ("baidu.com");

    }

}

 

web.xml

<filter>

    <filter-name>ReplaceSiteNameFilter</filter-name>

    <filter-class>com.zj.sample.ReplaceSiteNameFilter</filter-class>

</filter>

<filter-mapping>

    <filter-name>ReplaceSiteNameFilter</filter-name>

    <url-pattern>/login.jsp</url-pattern>

</filter-mapping>

 

测试结果:
过滤前

 

过滤后

8.压缩过滤器

有几个最新的浏览器可处理压缩的内容,自动解开以gzip作为Content-Encoding响应头值的压缩文件,然后就像对原文档那样处理结果。发送这样的压缩内容可以节省很多时间,因为在服务器上压缩文档,然后在客户机上解开文档所需的时间与下载文件的时间相比是微不足道的。程序LongServlet.java给出了一个具有很长的、重复的纯文本输出的servlet,这是一个可供压缩使用的成熟的servlet。如果使用gzip,它可以把输出结果压缩到1/300
在浏览器支持这个压缩能力时,压缩过滤器可利用章节7介绍的CharArrayWrapper来压缩内容,完成此任务需要下列内容:
1)实现Filter接口的类。这个类名为CompressionFIlterinit方法存放FilterConfig对象在一个字段中,以防子类需要访问servlet环境或过滤器名。destory方法体为空。
2)包装的响应对象。DoFilter方法将ServletResponse对象包装在一个CharArrayWrapper中,并传递此包装器到FilterChain对象的doFilter方法上。在此调用完成后,所有其他过滤器和最终资源都已执行,且输出结果位于包装器之内。这样,原doFilter提取一个代表所有资源的输出的字符数组。如果客户机指出它支持压缩(即,以gzip作为Accept-Encoding头的一个值),则过滤器附加一个GZIPOutputStreamByteArrayOutputStream上,将字符数组复制到此流中,并设置Content-Encoding响应头为gzip。如果客户机不支持gzip,则将未修改过的字符数组复制到ByteArrayOutputStream。最后,doFilter通过将整个字符数组(可能是压缩过的)写到与original响应相关的OutputStream中,发送结果到客户机。
3)对LongServlet进行注册。
CompressionFilter.java

package com.zj.sample;

import java.io.ByteArrayOutputStream;

import java.io.IOException;

import java.io.OutputStream;

import java.io.OutputStreamWriter;

import java.util.zip.GZIPOutputStream;

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.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

/**

 * Filter that compresses output with gzip (assuming that browser supports

 * gzip).

 */

public class CompressionFilter implements Filter {

 

    private FilterConfig config;

 

    /**

     * If browser does not support gzip, invoke resource normally. If browser

     * <I>does</I> support gzip, set the Content-Encoding response header and

     * invoke resource with a wrapped response that collects all the output.

     * Extract the output and write it into a gzipped byte array. Finally, write

     * that array to the client's output stream.

     */

    public void doFilter(ServletRequest request, ServletResponse response,

           FilterChain chain) throws ServletException, IOException {

       HttpServletRequest req = (HttpServletRequest) request;

       HttpServletResponse res = (HttpServletResponse) response;

       if (!isGzipSupported(req)) {

           // Invoke resource normally.

           chain.doFilter(req, res);

       } else {

           // Tell browser we are sending it gzipped data.

           res.setHeader("Content-Encoding", "gzip");

           // Invoke resource, accumulating output in the wrapper.

           CharArrayWrapper responseWrapper = new CharArrayWrapper(res);

           chain.doFilter(req, responseWrapper);

           // Get character array representing output.

           char[] responseChars = responseWrapper.toCharArray();

           // Make a writer that compresses data and puts it into a byte array.

           ByteArrayOutputStream byteStream = new ByteArrayOutputStream();

           GZIPOutputStream zipOut = new GZIPOutputStream(byteStream);

           OutputStreamWriter tempOut = new OutputStreamWriter(zipOut);

           // Compress original output and put it into byte array.

           tempOut.write(responseChars);

           // Gzip streams must be explicitly closed.

           tempOut.close();

           // Update the Content-Length header.

           res.setContentLength(byteStream.size());

           // Send compressed result to client.

           OutputStream realOut = res.getOutputStream();

           byteStream.writeTo(realOut);

       }

    }

 

    /**

     * Store the FilterConfig object in case subclasses want it.

     */

    public void init(FilterConfig config) throws ServletException {

       this.config = config;

    }

 

    protected FilterConfig getFilterConfig() {

       return (config);

    }

 

    public void destroy() {}

 

    private boolean isGzipSupported(HttpServletRequest req) {

       String browserEncodings = req.getHeader("Accept-Encoding");

       return ((browserEncodings != null) && (browserEncodings.indexOf("gzip") != -1));

    }

}

 

LongServlet.java

package com.zj.sample;

import java.io.IOException;

import java.io.PrintWriter;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

/**

 * Servlet with <B>long</B> output. Used to test the effect of the compression

 * filter of Chapter 9.

 */

 

public class LongServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)

           throws ServletException, IOException {

       response.setContentType("text/html");

       PrintWriter out = response.getWriter();

       String docType = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 "

              + "Transitional//EN\">\n";

       String title = "Long Page";

       out.println(docType + "<HTML>\n" + "<HEAD><TITLE>" + title

              + "</TITLE></HEAD>\n" + "<BODY BGCOLOR=\"#FDF5E6\">\n"

              + "<H1 ALIGN=\"CENTER\">" + title + "</H1>\n");

       String line = "Blah, blah, blah, blah, blah. "

              + "Yadda, yadda, yadda, yadda.";

       for (int i = 0; i < 10000; i++) {

           out.println(line);

       }

       out.println("</BODY></HTML>");

    }

}

 

web.xml

<filter>

    <filter-name>CompressionFilter</filter-name>

    <filter-class>com.zj.sample.CompressionFilter</filter-class>

</filter>

<filter-mapping>

    <filter-name>CompressionFilter</filter-name>

    <servlet-name>LongServlet</servlet-name>

</filter-mapping>

 

<servlet>

    <servlet-name>LongServlet</servlet-name>

    <servlet-class>com.zj.sample.LongServlet</servlet-class>

</servlet>

<servlet-mapping>

    <servlet-name>LongServlet</servlet-name>

    <url-pattern>/LongServlet</url-pattern>

</servlet-mapping>