Filter是攔截Request請求的對象:在用戶的請求訪 問資源前處理ServletRequest以及ServletResponse,它可 用於日志記錄、加解密、Session檢查、圖像文件保護 等。通過Filter可以攔截處理某個資源或者某些資源。 Filter的配置可以通過Annotation或者部署描述來完成。 當一個資源或者某些資源需要被多個Filter所使用到, 且它的觸發順序很重要時,只能通過部署描述來配置。
一.Filter API
1. Filter 相關接口,包含Filter, FilterConfig, FilterChain
Filter的實現必須繼承javax.servlet.Filter接口。這個 接口包含了Filter的3個生命周期:init、doFilter、 destroy。
Servlet容器初始化Filter時,會觸發Filter的init方 法,一般來說是在應用開始時。也就是說,init方法並 不是在該Filter相關的資源使用到時才初始化的,而且 這個方法只調用一次,用於初始化Filter。init方法的定 義如下:
oid init(FilterConfig filterConfig)
注意: FilterConfig實例是由Servlet容器傳入init方法中的.
當Servlet容器每次處理Filter相關的資源時,都會調 用該Filter實例的doFilter方法。Filter的doFilter方法包含 ServletRequest、ServletResponse、FilterChain這3個參 數。
doFilter的定義如下:
void doFilter(ServletRequest request, ServletResponse response,FilterChain filterChain)
接下來,說明一下doFilter的實現中訪問 ServletRequet、ServletResponse。這也就意味着允許給 ServletRequest增加屬性或者增加Header。當然也可以修 飾ServletRequest或者ServletRespone來改變它們的行 為。
在Filter的doFilter的實現中,最后一行需要調用 FilterChain中的doFilter方法。注意Filter的doFilter方法 里的第3個參數,就是filterChain的實例:
filterChain.doFilter(request, response)
一個資源可能需要被多個Filter關聯到(更專業一 點來說,這應該叫作Filter鏈條),這時Filter.doFilter() 的方法將觸發Filter鏈條中下一個Filter。只有在Filter鏈 條中最后一個Filter里調用的FilterChain.doFilter(),才會 觸發處理資源的方法。
如果在Filter.doFilter()的實現中,沒有在結尾處調 用FilterChain.doFilter()的方法,那么該Request請求中 止,后面的處理就會中斷。
注意: FilterChain接口中,唯一的方法就是doFilter。該方法與Filter中的 doFilter的定義是不一致的:在FilterChaing中,doFilter方法只有兩個參 數,但在Filter中,doFilter方法有三個參數。
Filter接口中,最后一個方法是destroy,它的定義 如下:
Void destroy()
該方法在Servlet容器要銷毀Filter時觸發,一般在應 用停止的時候進行調用。
除非Filter在部署描述中被多次定義到,否則Servlet 窗口只會為每個Filter創建單一實例。由於Serlvet/JSP的 應用通常要處理用戶並發請求,此時Filter實例需要同 時被多個線程所關聯到,因此需要非常小心地處理多線 程問題。
三. Filter配置
當完成Filter的實現后,就可以開始配置Filter了。 Filter的配置需要如下步驟:
- 確認哪些資源需要使用這個Filter攔截處理。
- 配置Filter的初始化參數值,這些參數可以在Filter的 init方法中讀取到;
- 給Filter取一個名稱。一般來說,這個名稱沒有什么 特別的含義,但在一些特殊的情況下,這個名字十 分有用。例如,要記錄Filter的初始化時間,但這個 應用中有許多的Filter,這時它就可以用來識別Filter 了。
FilterConfig接口允許通過它的getServletContext的 方法來訪問ServletContext:
ServletContext getServletContext(
如果配置了Filter的名字,在FilterConfig的 getFilterName中就可以獲取Filter的名字。getFilterName 的定義如下:
java.lang.String getFilterName()
當然,最重要的還是要獲取到開發者或者運維給 Filter配置的初始化參數。為了獲取這些初始化參數, 需要用到FilterConfig中的兩個方法,第一個方法是 getParameterNames:
java.util.Enumeration<java.lang.String> getInitParameterNames()
這個方法返回Filter參數名字的Enumeration對象。 如果沒有給這個Filter配置任何參數,該方法返回的是 空的Enumeration對象。
第二個方法是getParameter:
java.lang.String getInitParameter(java.lang.String parameterName)
有兩種方法可以配置Filter:一種是通過WebFilter 的Annotation來配置Filter,另一種是通過部署描述來注 冊。使用@WebFilter的方法,只需要在Filter的實現類 中增加一個注解即可,不需要重復地配置部署描述。當 然,此時要修改配置參數,就需要重新構建Filter實現 類了。換句話說,使用部署描述意味着修改Filter配置 只要修改一下文本文件就可以了。
使用@WebFilter,你需要熟悉下表中所列出來的 參數,這些參數是在WebFilter的Annotation里定義的。 所有參數都是可選的。
| 屬性 | 描述 |
| asyncSupported | Filter是否支持異步操作 |
| description | Filter的描述 |
| dispatcerTypes | Filter所生效范圍 |
| displayName | Filter的顯示名 |
| filterName | Filter的名稱 |
| initParams | Filter的初始化參數 |
| largeIcon | Filter的大圖名稱 |
| servletName | Filter所生效的Servlet名稱 |
| smallIcon | Filter的小圖名稱 |
| urlPatterns | Filter所生效的URL路徑 |
| value | Filter所生效的URL路徑 |
三. 示例1: 日志 Filter
作為第1個例子,將做一個簡單的Filter:在app09a 的應用中把Request請求的URL記錄到日志文本文件 中。日志文本文件名通過Filter的初始化參數來配置。 此外,日志的每條記錄都會有一個前綴,該前綴也由 Filter初始化參數來定義。通過日志文件,可以獲得許 多有用的信息,例如在應用中哪些資源訪問最頻繁; Web站點在一天中的哪個時間段訪問量最多。
這個Filter的類名叫LoggingFilter。 一般情況下,Filter的類名都以*Filter結尾。
package filter; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.util.Date; 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.annotation.WebFilter; import javax.servlet.annotation.WebInitParam; import javax.servlet.http.HttpServletRequest; @WebFilter(filterName = "LoggingFilter", urlPatterns = { "/ *" }, initParams = { @WebInitParam(name = "logFileName", value = "log.txt"), @WebInitParam(name = "prefix", value = "URI: ") }) public class LoggingFilter implements Filter { private PrintWriter logger; private String prefix; @Override public void init(FilterConfig filterConfig) throws ServletException { prefix = filterConfig.getInitParameter("prefix"); //得到URI String logFileName = filterConfig.getInitParameter("logFileName"); String appPath = filterConfig.getServletContext().getRealPath("/"); //得到項目路徑 // without path info in logFileName, the log file will be // created in $TOMCAT_HOME/bin System.out.println("logFileName:" + logFileName); try { logger = new PrintWriter(new File(appPath, logFileName)); //打開文件 logger.println("I have the output"); logger.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); throw new ServletException(e.getMessage()); } } @Override public void destroy() { System.out.println("destroying filter"); if (logger != null) { logger.close(); } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { System.out.println("LoggingFilter.doFilter"); HttpServletRequest httpServletRequest = (HttpServletRequest) request; logger.println(new Date() + " " + prefix + httpServletRequest.getRequestURI()); //寫入數據 logger.flush(); //刷新緩沖 filterChain.doFilter(request, response); //如果沒有doFilter方法,后面發Filter處理就會中斷 } }
下面來仔細分析一下Filter類。 首先,該Filter的類實現了Filter的接口並聲明兩個 變量:PrintWriter類型的logger和String類型的prefix。
其中PrintWriter用於記錄日志到文本文件,prefix的 字符串用於每條日志的前綴。 Filter的類使用了@WebFilter的Annotation,將兩個 參數(logFilteName、prefix)傳入到該Filter中.
在Filter的init方法中,通過FilterConfig里傳入的 getInitParameter方法來獲取prefix和getFileName的初始 化參數。其中把prefix參數中賦給了類變量prefix, logFileName則用於創建一個PrintWriter
如果Servlet/JSP應用是通過Servlet/JSP容器啟動 的,那么當前應用的工作目錄是當前JDK所在的目錄。 如果是在Tomcat中,該目錄是Tomcat的安裝目錄。在 應用中創建日志文件,可以通過 ServletContext.getRealPath來獲取工作目錄,結合應用 工作目錄以及初始化參數中的logFilterNmae,就可以得 到日志文件的絕對路徑.
當Filter的init方法被執行時,日志文件就會創建出 來。如果在應用的工作目錄中該文件已經存在,那么該 日志文件的內容將會被覆蓋。 當應用關閉時,PrintWriter需要被關閉。因此在 Filter的destroy方法中,需要:關閉日志
Filter的doFilter實現中記錄着所有從ServletRequest 到HttpServletRequest的Request,並調用了它的 getRequestURI方法,該方法的返回值將記錄通過 PrintWriter的pringln記錄下來
每條記錄都有一個時間戳以及前綴,這樣可以很方 便地標識每條記錄。接下來 Filter的doFilter實現調用 PrintWriter的flush方法以及FilterChain.doFilter,以喚起 資源的調用:
如果使用Tomcat,Filter的初始化並不會等到第一 個Request請求時才觸發進行。這點可以在控制台中打 印出來的logFileName參數值中可以看到。在app09a應 用中通過URL調用test.jsp頁面,就可以測試該Filter了
通過檢查日志文件的內容,就可以驗證這個Filter 是否運行正常。
四. 示例2:圖像文件保護Filter
本例中的圖像文件保護Filter用於在瀏覽器中輸入 圖像文件的URL路徑時,防止下載圖像文件。應用中的 圖像文件只有當圖像鏈接在頁面中被點擊的時候才會顯 示。該Filter的實現原理是檢查HTTP Header的referer 值。如果該值為null,就意味着當前的請求中沒有 referer值,即當前的請求是直接通過輸入URL來訪問該 資源的。如果資源的Header值為非空,將返回Request 語法的原始頁面作為referer值。注意Header的referer的 屬性名中,在第2個e以及第3個e中僅有一個r。
ImageProtectorFilter的Filter實現類,如清單所 示。從WebFilter的Annotation中,可以看到該Filter應用 於所有的.png、.jpg、.gif文件后綴。
ImageProtectorFilter實現類
package filter; import java.io.IOException; 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.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; //過濾所有以.png , .jpg .gif 為后綴的訪問 @WebFilter(filterName = "ImageProtetorFilter", urlPatterns = { "*.png", "*.jpg", "*.gif" }) public class ImageProtectorFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { //直接輸入圖片地址訪問http://localhost:8080/app09/圖片.jpg會被攔截 System.out.println("ImageProtectorFilter"); HttpServletRequest httpServletRequest = (HttpServletRequest) request; String referrer = httpServletRequest.getHeader("referer"); System.out.println("referrer:" + referrer); if (referrer != null) {
filterChain.doFilter(request, response); } else { throw new ServletException("Image not available"); } } }
這里並沒有init和destroy方法。其中doFilter方法讀 取到Header中的referer值,要確認是要繼續處理這個資 源還是給個異常.
測試該Filter,可以在瀏覽器中輸入如下ULR路 徑,嘗試訪問logo.png圖像:
http://localhost:8080/app09/圖片.jpg

接下來,通過image.jsp的頁面來訪問該圖像:
http://localhost:8080/app09/test.jsp

五.示例3:下載計數Filter
本例子中,下載計數Filter將會示范如何在Filter中 計算資源下載的次數。這個示例特別有用,它將會得到 文檔、音頻文件的受歡迎程度。作為簡單的示例,這里 將數值保存在屬性文件中,而不保存在數據庫中。其中 資源的ULR路徑將作為屬性名保存在屬性文件中。
因為我們把值保存在屬性文件中,並且Filter可以 被多線程訪問,因此涉及線程安全問題。用戶訪問一個 資源時,Filter需要讀取相應的屬性值加1,然后保存該 值。如果第二個用戶在第一個線程完成前同時訪問該資 源,將會發生什么呢?計算值出錯。在本例中,讀寫的 同步鎖並不是一個好的解決這個問題的方法,因為它會 導致擴展性問題。
本示例中,解決這個線程安全問題是通過Queue以 及Executor。
簡而言之,進來的Request請求將會保存在單線程 Executor的隊列中。替換這個任務十分方便,因為這是 一個異步的方法,因此你不需要等待該任務結束。 Executor一次從隊列中獲取一個對象,然后做相應屬性 值的增加。由於Executor只在一個線程中使用,因此可 以消除多個線程同時訪問一個屬性文件的影響。
DownloadCounterFilter實現類
package filter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; 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.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; // 攔截所有URL //書上的是 urlPatterns={"/"},親測無效 @WebFilter(filterName = "DownloadCounterFilter", urlPatterns = { "*.JPG" }) public class DownloadCounterFilter implements Filter { // 獲取單個線程池的Executor 由於Executor只在一個線程中使用,因此可 以消除多個線程同時訪問一個屬性文件的影響。 ExecutorService executorService = Executors.newSingleThreadExecutor(); // propeties 屬性集 Properties downloadLog; File logFile; @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("DownloadCounterFilter"); //獲取路徑 String appPath = filterConfig.getServletContext().getRealPath("/"); // 獲取downLoadLog.txt文件 logFile = new File(appPath, "downloadLog.txt"); if (!logFile.exists()) { try { logFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } downloadLog = new Properties(); try { downloadLog.load(new FileReader(logFile)); } catch (IOException e) { e.printStackTrace(); } } @Override public void destroy() { executorService.shutdown(); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; final String uri = httpServletRequest.getRequestURI(); executorService.execute(new Runnable() { @Override public void run() { String property = downloadLog.getProperty(uri); if (property == null) { downloadLog.setProperty(uri, "1"); } else { int count = 0; try { count = Integer.parseInt(property); } catch (NumberFormatException e) { // silent } count++; downloadLog.setProperty(uri, Integer.toString(count)); } try { downloadLog.store(new FileWriter(logFile), ""); } catch (IOException e) { } } }); filterChain.doFilter(request, response); } }
如果在當前應用的工作目錄中不存在 downloadLog.txt文件,這個Filter的init方法就會創建 它
接着創建Properties對象,並讀取該文件.
注意,Filter的實現類中引用到了 ExecutorService(Executor的子類).
且當Filter銷毀時,會調用ExecutorService的 shutdown方法.
Filter的doFilter實現中大量地使用到這個Job。每次 URL請求都會調用到ExecutorService的execute方法,然 后才調用FilterChaing.doFilter()。該任務的execute實現 非常好理解:它將URL作為一個屬性名,從Properties 實例中獲取該屬性的值,然后加1,並調用flush方法寫 回到指定的日志文件中
這個Filter可在許多資源上生效,但也可以非常簡 單地配置,限定為PDF或者AVI文件資源。
properties文件

六. Filter順序
如果多個Filter應用於同一個資源,Filter的觸發順 序將變得非常重要,這時就需要使用部署描述來管理 Filter:指定哪個Filter先被觸發。例如:Filter 1需要在 Filter 2前被觸發,那么在部署描述中,Filter 1需要配置 在Filter 2之前:
<filter> <filter-name>Filter1</filter-name> <filter-class> the fully-qualified name of the filter class </filter-class> </filter> <filter> <filter-name>Filter2</filter-name> <filter-class> the fully-qualified name of the filter class </filter-class> </filter>
通過部署描述之外的配置來指定Filter觸發的順序 是不可能的
