過濾器(Filter)的概念
- 過濾器位於客戶端和web應用程序之間,用於檢查和修改兩者之間流過的請求和響應。
- 在請求到達Servlet/JSP之前,過濾器截獲請求。
- 在響應送給客戶端之前,過濾器截獲響應。
- 多個過濾器形成一個過濾器鏈,過濾器鏈中不同過濾器的先后順序由部署文件web.xml中過濾器映射<filter-mapping>的順序決定。
- 最先截獲客戶端請求的過濾器將最后截獲Servlet/JSP的響應信息。
過濾器的鏈式結構
可以為一個Web應用組件部署多個過濾器,這些過濾器組成一個過濾器鏈,每個過濾器只執行某個特定的操作或者檢查。這樣請求在到達被訪問的目標之前,需要經過這個過濾器鏈。

實現過濾器
在Web應用中使用過濾器需要實現javax.servlet.Filter接口,實現Filter接口中所定義的方法,並在web.xml中部署過濾器。
public class MyFilter implements Filter {
public void init(FilterConfig fc) {
//過濾器初始化代碼
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
//在這里可以對客戶端請求進行檢查
//沿過濾器鏈將請求傳遞到下一個過濾器。
chain.doFilter(request, response);
//在這里可以對響應進行處理
}
public void destroy( ) {
//過濾器被銷毀時執行的代碼
}
}
Filter接口
public void init(FilterConfig config)
web容器調用本方法,說明過濾器正被加載到web容器中去。容器只有在實例化過濾器時才會調用該方法一次。容器為這個方法傳遞一個FilterConfig對象,其中包含與Filter相關的配
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
每當請求和響應經過過濾器鏈時,容器都要調用一次該方法。需要注意的是過濾器的一個實例可以同時服務於多個請求,特別需要注意線程同步問題,盡量不用或少用實例變量。 在過濾器的doFilter()方法實現中,任何出現在FilterChain的doFilter方法之前地方,request是可用的;在doFilter()方法之后response是可用的。
public void destroy()
容器調用destroy()方法指出將從服務中刪除該過濾器。如果過濾器使用了其他資源,需要在這個方法中釋放這些資源。
部署過濾器
在Web應用的WEB-INF目錄下,找到web.xml文件,在其中添加如下代碼來聲明Filter。
<filter>
<filter-name>TlwModifyResponseFilter</filter-name>
<filter-class>
com.Common.action.TlwModifyResponseFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>TlwModifyResponseFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
以上是我的項目工程中的action路徑
1 <filter-mapping> 2 <filter-name>Logging Filter</filter-name> 3 <url-pattern>/products/*</url-pattern> 4 </filter-mapping>
這種情況下,過濾器將會作用於直接從客戶端發過來的以/products/…開始的請求。因為這里沒有制定任何的<dispatcher>元素,默認值是REQUEST。
例2:
<filter-mapping> <filter-name>Logging Filter</filter-name> <servlet-name>ProductServlet</servlet-name> <dispatcher>INCLUDE</dispatcher> </filter-mapping>
這種情況下,如果請求是通過request dispatcher的include方法傳遞過來的對ProductServlet的請求,則要經過這個過濾器的過濾。其它的諸如從客戶端直接過來的對ProductServlet的請求等都不需要經過這個過濾器。
指定filter的匹配方式有兩種方法:直接指定url-pattern和指定servlet,后者相當於把指定的servlet對應的url-pattern作為filter的匹配模式,filter的路徑匹配和servlet是一樣的,都遵循servlet規范中《SRV.11.2 Specification of Mappings》一節的說明 。
例3:
<filter-mapping> <filter-name>Logging Filter</filter-name> <url-pattern>/products/*</url-pattern> <dispatcher>FORWARD</dispatcher> <dispatcher>REQUEST</dispatcher> </filter-mapping>
在這種情況下,如果請求是以/products/…開頭的,並且是通過request dispatcher的forward方法傳遞過來或者直接從客戶端傳遞過來的,則必須經過這個過濾器。
1.請求過濾器
web.xml中配置如下
<filter> <filter-name>MyFilter</filter-name> <filter-class>cn.telling.Filter.MyFilter</filter-class> </filter> <filter-mapping> <filter-name>MyFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
public class MyFilter implements Filter{ FilterConfig config; /** * * @Description: TODO * @param filterConfig * @throws ServletException * @author xingle * @data 2015-10-26 下午4:32:44 */ @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("begin do the log filter!"); this.config = filterConfig; } /** * * @Description: TODO * @param request * @param response * @param chain * @throws IOException * @throws ServletException * @author xingle * @data 2015-10-26 下午4:32:44 */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletContext context = this.config.getServletContext(); System.out.println("before the log filter!"); HttpServletRequest hreq = (HttpServletRequest) request; System.out.println("Log Filter已經截獲到用戶的請求的地址:"+hreq.getServletPath() ); // Filter 只是鏈式處理,請求依然轉發到目的地址。 chain.doFilter(request, response); } /** * * @Description: TODO * @author xingle * @data 2015-10-26 下午4:32:44 */ @Override public void destroy() { this.config = null; } }
2.響應過濾器
比如要實現輸出壓縮:

這樣不行!輸出會從servlet直接返回給客戶。但是我們的目標是壓縮輸出。
先來想想這樣一個問題…… servlet 實際上是從響應對象得到輸出流或書寫器。那么,如果不把實際的相應對象傳給servlet,而是由過濾器換入一個定制的相應對象,而且這個定制響應對象有你能控制的一個輸出流,這樣可以嗎?需要建立我們自己的HttpServletResponse 接口定制實現,並把它通過chain.doFilter() 調用傳遞到servlet。而且這個定制實現還必須包含一個定制輸出流,因為這正是我們的目標,在servlet寫輸出之后並且在輸出返回給客戶之前,過濾器就能拿到這個輸出。

servlet中使用HttpServletResponseWrapper截獲返回的頁面內容
要截獲頁面返回的內容,整體的思路是先把原始返回的頁面內容寫入到一個字符Writer,然后再組裝成字符串並進行分析,最后再返回給客戶端。代碼如下:
package cn.telling.Filter; import java.io.CharArrayWriter; import java.io.PrintWriter; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; /** * 自定義一個響應結果包裝器,將在這里提供一個基於內存的輸出器來存儲所有 * 返回給客戶端的原始HTML代碼。 * @ClassName: ResponseWrapper TODO * @author xingle * @date 2015-10-27 上午9:22:14 */ public class ResponseWrapper extends HttpServletResponseWrapper { private PrintWriter cachedWriter; private CharArrayWriter bufferedWriter; /** * @param response */ public ResponseWrapper(HttpServletResponse response) { super(response); // 這個是我們保存返回結果的地方 bufferedWriter = new CharArrayWriter(); // 這個是包裝PrintWriter的,讓所有結果通過這個PrintWriter寫入到bufferedWriter中 cachedWriter = new PrintWriter(bufferedWriter); } public PrintWriter getWriter(){ return cachedWriter; } /** * 獲取原始的HTML頁面內容。 * @return */ public String getResult() { return bufferedWriter.toString(); } }
然后再寫一個過濾器來截獲內容並處理:
package cn.telling.Filter; 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; /** * * @ClassName: MyServletFilter TODO * @author xingle * @date 2015-10-27 上午9:24:34 */ public class MyServletFilter implements Filter { /** * * @Description: TODO * @param filterConfig * @throws ServletException * @author xingle * @data 2015-10-27 上午9:24:47 */ @Override public void init(FilterConfig filterConfig) throws ServletException { // TODO Auto-generated method stub } /** * * @Description: TODO * @param request * @param response * @param chain * @throws IOException * @throws ServletException * @author xingle * @data 2015-10-27 上午9:24:47 */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 使用我們自定義的響應包裝器來包裝原始的ServletResponse ResponseWrapper wrapper = new ResponseWrapper((HttpServletResponse) response); // 這句話非常重要,注意看到第二個參數是我們的包裝器而不是response chain.doFilter(request, wrapper); // 處理截獲的結果並進行處理,比如替換所有的“名稱”為“鐵木箱子” String result = wrapper.getResult(); result = result.replace("名稱", "替換后的"); // 輸出最終的結果 PrintWriter out = response.getWriter(); out.write(result); out.flush(); out.close(); } /** * * @Description: TODO * @author xingle * @data 2015-10-27 上午9:24:47 */ @Override public void destroy() { // TODO Auto-generated method stub } }
然后將該servlet配置在web.xml文件中,如下:
<filter> <filter-name>MyFilter</filter-name> <filter-class>cn.telling.Filter.MyServletFilter</filter-class> </filter> <filter-mapping> <filter-name>MyFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
然后我們在web應用根目錄下建立一個jsp文件echo.jsp,內容如下:
<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <head> <title>頁面返回結果過濾測試</title></head> </head> <body> 你好,我叫“名稱”。 </body> </html>
配置完后,部署到tomcat,然后訪問應用下的echo.jsp文件,就可以發現返回的內容變成了:

從而也就達到了我們想要的效果了。在文章開頭我也提到了說有一個問題,那就是有可能在運行的過程中頁面只輸出一部分,尤其是在使用多個框架后(比如sitemesh)出現的可能性非常大,在探究了好久之后終於發現原來是響應的ContentLength惹的禍。因為在經過多個過濾器或是框架處理后,很有可能在其他框架中設置了響應的輸出內容的長度,導致瀏覽器只根據得到的長度頭來顯示部分內容。知道了原因,處理起來就比較方便了,我們在處理結果輸出前重置一下ContentLength即可,如下:
// 重置響應輸出的內容長度 response.setContentLength(-1); // 輸出最終的結果 PrintWriter out = response.getWriter(); out.write(result); out.flush(); out.close();
這樣處理后就不會再出現只出現部分頁面的問題了!
