過濾是 Tomcat 4 的新功能。它是 Servlet 2.3 規范的一部分,並且最終將 為所有支持此標准的 J2EE 容器的廠商所采用執行。開發人員將能夠用過濾器來 實現以前使用不便的或難以實現的功能,這些功能包括:
資源訪問(Web 頁、JSP 頁、servlet)的定制身份認證
應用程序級的訪問資源的審核和記錄
應用程序范圍內對資源的加密訪問,它建立在定制的加密方案基礎上
對被訪問資源的及時轉換,包括從 servlet 和 JSP 的動態輸出
這個清單當然並沒有一一羅列,但它讓您初步體驗到了過濾所帶來的額外價 值。在本文中,我們將詳細討論 Servlet 2.3 的過濾,來看一看過濾器是如何 配合 J2EE 處理模型的。不像其它傳統的過濾方案,Servlet 2.3過濾是建立在 嵌套調用的基礎上的。我們來研究一下這一差別是怎樣在架構上與新的高性能 Tomcat 4 設計取得一致的。最後,我們將獲得一些編寫及測試兩個 Servlet 2.3過濾器的實際經驗。這些過濾器只完成很簡單的功能,使我們得以將注意力 集中於編寫過濾器以及如何將它們集成進 Web 應用程序的機制。
作為 Web 應用程序構建模塊的過濾器
在物理結構上,過濾器是 J2EE Web 應用程序中的應用程序級的 Java 代碼 組件。除了 servlet 和 JSP 頁以外,遵循 Servlet 2.3 規范編碼的開發人員 能將過濾器作為在 Web 應用程序中加入活動行為的機制。與在特定的 URL 上工 作的 servlet 和 JSP 頁不同,過濾器接入 J2EE 容器的處理管道,並能跨越由 Web 應用程序提供的 URL 子集(或所有 URL)進行工作。圖 1 說明了過濾是在 哪裡配合 J2EE 請求處理的。
圖 1.過濾器與 J2EE 請求處理
兼容 Servlet 2.3 的容器允許過濾器在請求被處理(通過 Servlet 引擎) 以前以及請求得到處理 以後(過濾器將可以訪問響應)訪問 Web 請求。
在這些情況下,過濾器可以:
在請求得到處理以前修改請求的標題
提供它自己的請求版本以供處理
在請求處理以後和被傳回給用戶以前修改響應
先取得由容器進行的所有請求處理,並產生自己的響應
比過濾器的可用性更為重要的是,接入 J2EE 處理管道需要創建不可移植的 、容器專用的和系統范圍的擴展機制(如 Tomcat 3 攔截器)。
概念上的 Tomcat過濾
不同於在 Apache、IIS 或 Netscape 服務器中能找到的熟悉的過濾機制, Servlet 2.3過濾器並非建立在掛鉤式函數調用上。事實上, Tomcat 4 級別的 引擎架構脫離了傳統的 Tomcat 3.x 版本。新的 Tomcat 4 引擎取代了在請求處 理的不同階段調用掛鉤式方法的整體式引擎,它在內部使用了一系列的嵌套調用、包裝請求及響應。不同的過濾器和資源處理器構成了一個鏈。
在傳統架構中:
每次接受到請求,掛鉤式方法就被調用,不論它們是否執行(有時甚至是空 的)。
方法的作用域及並發關系(每個方法可能在不同的線程上被調用)不允許在 處理相同的請求時簡單、高效地共享不同掛鉤式方法調用間的變量和信息。
在新架構中:
嵌套的方法調用通過一系列過濾器實現,它僅有應用於當前請求的過濾器組 成;基於掛鉤式調用的傳統執行方式需要在處理短句中調用掛鉤式例程,即使一 個特定短句的處理邏輯不起任何作用。
局部變量在實際的過濾方法返回之前都作保留,並且可用(因為上游過濾器 的調用總在堆棧上,等待後續調用的返回)。
這一新架構為今後的 Tomcat 性能調整與優化提供了一個新的、更 對象友好 的基礎。Servlet 2.3過濾器是這個新的內部架構的自然擴展。該架構為 Web 應用程序設計人員提供了一個可移植的執行過濾行為的方法。
調用鏈
所有過濾器都服從調用的過濾器鏈,並通過定義明確的接口得到執行。一個 執行過濾器的 Java 類必須執行這一 javax.servlet.Filter 接口。這一接口含 有三個過濾器必須執行的方法:
doFilter(ServletRequest, ServletResponse, FilterChain) :這是一個完 成過濾行為的方法。這同樣是上游過濾器調用的方法。引入的 FilterChain 對 象提供了後續過濾器所要調用的信息。
init(FilterConfig) :這是一個容器所調用的初始化方法。它保證了在第一 次 doFilter() 調用前由容器調用。您能獲取在 web.xml 文件中指定的初始化 參數。
destroy() :容器在破壞過濾器實例前, doFilter() 中的所有活動都被該 實例終止後,調用該方法。
請注意: Filter 接口的方法名及語義在最近的幾個 beta 周期中曾有過不 斷的改變。Servlet 2.3 規范仍未處於最後的草案階段。在 Beta 1 中,該接口 包括 setFilterConfig() 和 getFilterConfig() 方法,而不是 init() 和 destroy() 。
嵌套調用在 doFilter() 方法執行中發生。除非您建立一個過濾器明確阻止 所有後續處理(通過其它過濾器及資源處理器),否則過濾器一定會在 doFilter 方法中作以下的調用:
FilterChain.doFilter(request, response);
安裝過濾器:定義與映射
容器通過 Web 應用程序中的配置描述符 web.xml 文件了解過濾器。有兩個 新的標記與過濾器相關: <filter> 和 <filter-mapping> 。應該 指定它們為 web.xml 文件內 <web-app> 標記的子標記。
過濾器定義的元素
<filter> 標記是一個過濾器定義,它必定有一個 <filter- name> 和 <filter-class> 子元素。<filter-name> 子元素給 出了一個與過濾器實例相關的、基於文本的名字。<filter-class> 指定 了由容器載入的實際類。您能隨意地包含一個 <init-param> 子元素為過 濾器實例提供初始化參數。例如,下面的過濾器定義指定了一個叫做 IE Filter 的過濾器:
清單 1.過濾器定義標記
<web-app>
<filter>
<filter-name>IE Filter</filter-name>
<filter-class>com.ibm.devworks.filters.IEFilter</filter- class>
</filter>
</web-app>
容器處理 web.xml 文件時,它通常為找到的每個過濾器定義創建一個過濾器 實例。這一實例用來服務所有的可用URL 請求;因此,以線程安全的方式編寫過濾器是最為重要的。
過濾器映射及子元素
<filter-mapping> 標記代表了一個過濾器的映射,指定了過濾器會對 其產生作用的 URL 的子集。它必須有一個 <filter-name> 子元素與能找 到您希望映射的過濾器的過濾器定義相對應。接下來,您可以使用<servlet-name> 或 <url-pattern> 子元素來指定映射。 <servlet-name> 指定了一個過濾器應用的 servlet (在 web.xml 文件 中的其它地方已定義)。您能使用<url-pattern> 來指定一個該過濾器 應用的 URL 的子集。例如, /* 的樣式用來代表該過濾器映射應用於該應用程 序用到的每個 URL,而 /dept/humanresources/* 的樣式則表明該過濾器映射只 應用於人力資源部專有的 URL。
容器使用這些過濾器映射來確定一個特定的過濾器是否應參與某個特定的請 求。清單 1 是為應用程序的所有 URL 定義的應用於 IE Filter 的一個過濾器 映射:
清單 2.過濾器映射標記
<filter-mapping>
<filter-name>IE Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
創建一個簡單的過濾器
現在該來定義我們的第一個過濾器了。這是一個不重要的過濾器,檢查請求 標題以確定是不是使用Internet Explorer 浏覽器來查看 URL 的。如果是 Internet Explorer 浏覽器,過濾器就顯示“拒絕訪問”的信息。盡管操作並不 重要,但這個示例演示了:
一個過濾器的一般剖析
一個在請求到達資源處理器前檢查其標題信息的過濾器
如何編寫一個過濾器來阻止基於運行時間檢測到的條件(驗證參數、源 IP、 時間…等等)的後續處理
此過濾器的源代碼作為 IEFilter.java , com.ibm.devworks.filters 包的 一部分位於源代碼發布區中。現在就讓我們來仔細研究一下該過濾器的代碼。
清單 3. 使用Filter 接口
public final class IEFilter implements Filter {
private FilterConfig filterConfig = null;
所有的過濾器都須執行 Filter 接口。我們創建了一個局部變量以容納由容 器在初始化過濾器時傳遞進來的 filterConfig 。這有時發生在第一次調用doFilter() 前。
清單 4. doFilter 方法
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
String browserDet =
((HttpServletRequest) request).getHeader("User- Agent").toLowerCase();
if ( browserDet.indexOf("msie") != -1) {
PrintWriter out = response.getWriter();
out.println ("<html><head></head><body>");
out.println("<h1>Sorry, page cannot be displayed!</h1>");
out.println("</body></html>");
out.flush();
return;
}
doFilter() 完成了大部分工作。我們來檢查一下叫做“用戶代理”標題的請 求標題。所有的浏覽器都提供這個標題。我們將其轉換成小寫字母,然後查找說 明問題的標識字符串 "msie"。如果檢測到了 Internet Explorer,我們就從響 應對象中獲取一個 PrintWriter 來寫出自己的響應。在寫出了定制的響應後, 方法無需連到其它過濾器就能返回。這就是過濾器阻止後續處理的方法。
如果浏覽器並非 Internet Explorer,我們就能進行正常的鏈式操作,讓後 續過濾器和處理器能在得到請求時獲得執行的機會:
清單 5. 進行正常鏈式操作
chain.doFilter(request, response);
}
隨後,我們粗略地執行該過濾器中的 init() 和 destroy() 方法:
清單 6. init() 和 destroy() 方法
public void destroy() {
}
public void init(FilterConfig filterConfig) {
this.filterConfig = filterConfig;
}
}
測試 IEFilter
假設您安裝了 Tomcat 4 beta 3 (或更新版本)並能使用,請按下列步驟啟 動 IEFilter 並運行:
在 $TOMCAT_HOME/conf 目錄下的 server.xml 文件裡創建一個新的應用程序 上下文,如下所示:
<!-- Tomcat Examples Context -->
<Context path="/examples" docBase="examples" debug="0"
reloadable="true">
...
</Context>
<Context path="/devworks" docBase="devworks" debug="0"
reloadable="true">
<Logger className="org.apache.catalina.logger.FileLogger"
prefix="localhost_devworks_log." suffix=".txt"
timestamp="true"/>
</Context>
編輯代碼區的 devworks/WEB-INF 下的 web.xml 文件,以包括下列的過濾器 定義及映射:
<web-app>
<filter>
<filter-name>IE Filter</filter-name>
<filter-class>com.ibm.devworks.filters.IEFilter</filter- class>
</filter>
<filter-mapping>
<filter-name>IE Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
在 $TOMCAT_HOME/webapps 目錄下創建一個叫做 devworks 的新目錄,並將 所有 devworks 目錄下的東西(包括所有子目錄)從源代碼區復制到該位置。現 在就准備好啟動 Tomcat 4 了。
使用下面的 URL 來訪問一個簡單的 index.html 頁面: http://<hostname>/devworks/index.html
如果您使用的是 Internet Explorer,就能看見如圖 2 所示的定制的“拒絕 訪問”信息。
圖 2. IEFilter 在遇到 Internet Explorer 的運行效果
如果您使用的是 Netscape,那就能看見如圖 3 所示的確切的 HTML 頁面。
圖 3. IEFilter 用Netscape 浏覽器的浏覽效果
編寫轉換資源的過濾器
現在該來試一下更復雜的過濾器了。該過濾器:
從過濾器定義的實例初始化參數中讀取一組 "search" 及 "replace" 文本
過濾被訪問的 URL,將出現的第一個 "search" 文本替代為 "replace" 文本
在我們深入研究這個過濾器的過程中,您將對內容轉換/替代過濾器的架構加 深了解。相同的架構能用於任何加密、壓縮及轉換(如由 XSLT 轉換來的 SML)過濾器。
核心機密是在鏈式處理的過程中傳遞一個定制的響應對象的包裝版本。該定 制的包裝響應對象須隱藏原響應對象(從而對其實現 包裝),並提供一個定制 的流以供後續處理器寫入。如果工作(文本替換、轉換、壓縮、加密…等)能迅 速完成,定制流的執行就能中止後續記錄並完成需要的工作。然後定制的流就會 將經轉換的數據寫入包裝的響應對象(也就是說,簡單的字符替換加密)。如果 工作無法迅速完成,定制的流就需等待,直到後續處理器完成對流的寫入(也就 是說,當其關閉或刷新流時)。然後它才完成轉換工作,並將經轉換的輸出結果 寫入“真正的”響應中。
在我們的過濾器( ReplaceTextFilter )中,定制的包裝響應對象叫作 ReplaceTextWrapper 。定制流的執行叫做 ReplaceTextStream 。您能在 com.ibm.devworks.filters 包中的 ReplaceTextFilter.java 文件裡找到源代 碼。現在就讓我們來研究一下源代碼吧。
清單 7. ReplaceTextStream 類
class ReplaceTextStream extends ServletOutputStream {
private OutputStream intStream;
private ByteArrayOutputStream baStream;
private boolean closed = false;
private String origText;
private String newText;
public ReplaceTextStream(OutputStream outStream,
String searchText,
String replaceText) {
intStream = outStream;
baStream = new ByteArrayOutputStream();
origText = searchText;
newText = replaceText;
}
這是定制的輸出流代碼。intStream 變量包含了對來自響應對象的實際流的 引用。baStream 是我們輸出流的緩沖版本,後續處理器就寫入這裡。closed 標 記標明了 close() 是否在此實例流中被調用。構造器將來自響應對象的流引用存儲起來並創建了緩沖流。它還將文本字符串存儲起來供以後的替代操作使用。
清單 8. write() 方法
public void write(int i) throws java.io.IOException {
baStream.write(i);
}
我們須提供自己的源於 ServletOutputStream 的 write() 方法。在此,我 們當然是寫入緩沖流。所有來自後續處理器的更高級輸出方法都將以最低級別使 用該方法,以保證所有的寫入都指向緩沖流。
清單 9. close() 及 flush() 方法
public void close() throws java.io.IOException {
if (!closed) {
processStream();
intStream.close();
closed = true;
}
}
public void flush() throws java.io.IOException {
if (baStream.size() != 0) {
if (! closed) {
processStream(); // need to synchronize the flush!
baStream = new ByteArrayOutputStream();
}
}
}
close() 及 flush() 方法是我們完成轉換的語句。根據後續處理器不同,其 中的一個或兩個程序都有可能被調用。我們使用布爾型的 closed 標識來避免異 常情況。請注意,我們將實際的替代工作委托給了 processStream() 方法。
清單 10. processStream() 方法
public void processStream() throws java.io.IOException {
intStream.write(replaceContent(baStream.toByteArray ()));
intStream.flush();
}
processStream() 方法將經轉換的輸出結果從 baStream 寫入其已經配有的 intStream 中去。轉換工作獨立於 replaceContent() 方法。
清單 11. replaceContent() 方法
public byte [] replaceContent(byte [] inBytes) {
String retVal ="";
String firstPart="";
String tpString = new String(inBytes);
String srchString = (new String (inBytes)).toLowerCase();
int endBody = srchString.indexOf(origText);
if (endBody != -1) {
firstPart = tpString.substring(0, endBody);
retVal = firstPart + newText +
tpString.substring(endBody + origText.length ());
} else {
retVal=tpString;
}
return retVal.getBytes();
}
}
replaceContent() 是發生搜索與替換的語句。它將一個字節數組作為輸入並 返回一個字節數組,創建一個原始的概念接口。事實上,我們能通過替換該方法 中的邏輯部分來完成任何形式的轉換。這裡,我們進行非常簡單的文本替換。
清單 12. ReplaceTextWrapper 類
class ReplaceTextWrapper extends HttpServletResponseWrapper {
private PrintWriter tpWriter;
private ReplaceTextStream tpStream;
public ReplaceTextWrapper(ServletResponse inResp, String searchText,
String replaceText)
throws java.io.IOException {
super((HttpServletResponse) inResp);
tpStream = new ReplaceTextStream (inResp.getOutputStream(),
searchText,
replaceText);
tpWriter = new PrintWriter(tpStream);
}
public ServletOutputStream getOutputStream() throws java.io.IOException {
return tpStream;
}
public PrintWriter getWriter() throws java.io.IOException {
return tpWriter;
}
}
我們定制的包裝響應能方便地從幫助類 HttpServletResponseWrapper 中導 出。這一類粗略地執行許多方法,允許我們簡單地覆蓋 getOutputStream() 方 法以及 getWriter() 方法,提供了定制輸出流的實例。
清單 13. ReplaceTextWrapper() 方法
public final class ReplaceTextFilter implements Filter {
private FilterConfig filterConfig = null;
private String searchText = ".";
private String replaceText = ".";
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
ReplaceTextWrapper myWrappedResp = new ReplaceTextWrapper( response,
searchText, replaceText);
chain.doFilter(request, myWrappedResp);
myWrappedResp.getOutputStream().close();
}
public void destroy() {
}
最後,還有過濾器本身。它所做的不過是使用FilterChain 為遞交響應後續 創建一個定制的包裝響應實例,如下所示:
清單 14. 創建一個定制的包裝響應實例
public void init (FilterConfig filterConfig) {
String tpString;
if (( tpString = filterConfig.getInitParameter("search") ) != null)
searchText = tpString;
if (( tpString = filterConfig.getInitParameter("replace") ) != null)
replaceText = tpString;
this.filterConfig = filterConfig;
}
}
在 init 方法中,我們取回了過濾器定義中指定的初始參數。filterConfig 對象中的 getInitParameter() 方法便於用來實現這個目的。
測試 ReplaceTextFilter
假如您使用先前提及的步驟測試了 IEFilter ,並將所有文件復制到了 $TOMCAT/webapps/devworks 下,您就能用以下的步驟來測試 ReplaceTextFilter :
編輯 $TOMCAT/wepapps/devworks/WEB-INF 目錄下的 web.xml 文件,以包含 下列過濾器的定義及映射:
<web-app>
<filter>
<filter-name>Replace Text Filter</filter-name>
<filter- class>com.ibm.devworks.filters.ReplaceTextFilter</filter- class>
<init-param>
<param-name>search</param-name>
<param-value>cannot</param-value>
</init-param>
<init-param>
<param-name>replace</param-name>
<param-value>must not</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Replace Text Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
重新啟動 Tomcat。
現在,請用下面的 URL 來訪問 index.html 頁面: http://<host name>:8080/devworks/index.html
請注意, ReplaceTextFilter 是如何迅速地將 cannot變為 must not 的。 想確信過濾使用了所有資源,您可以嘗試編寫輸出結果含有字符串 cannot的 JSP 頁或 servlet。
過濾器鏈排列順序的重要性
過濾器鏈式排列的順序取決於 web.xml 描述信息內 <filter- mapping> 語句的順序。在大多數情況下,過濾器鏈式排列的順序是非常重要 的。也就是說,在應用A過濾器前使用B過濾器與在使用B過濾器前使用A過濾器所得到的結果是完全不同的。如果一個應用程序中使用了一個以上的過濾 器,那麼在寫入 <filter-mapping> 語句的時候要小心。
我們能輕易地通過排列 web.xml 文件中 <filter-mapping> 的順序看 到這一效果:
清單 15.過濾的順序 -- IE Filter 為先
<web-app>
<filter-mapping>
<filter-name>IE Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>Replace Text Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
現在,用Internet Explorer 載入 index.html 頁。您能看到由於 IE Filter 處於過濾器鏈中的第一位,所以 Replace Text Filter 沒有機會執行。 因此,輸出的信息是 "Sorry, page cannot be displayed!"
現在,將 <filter-mapping> 標記的順序顛倒過來,變為:
清單 16.過濾的順序 -- Replace Text Filter 為先
<web- app>
<filter-mapping>
<filter-name>Replace Text Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>IE Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
再次用Internet Explorer 載入 index.html 頁面。這次, Replace Text Filter 先執行,將包裝的響應對象提供給 IE Filter 。在 IE Filter 寫入了 其定制的響應後,專用的響應對象在輸出結果到達最終用戶處以前完成轉換。故 而,我們看到了這條信息:Sorry, page must not be displayed!
在應用程序中使用過濾器
寫這篇文章的時候, Tomcat 4 正處於 beta 周期的後期,正式發行的日子 已為期不遠。主要的 J2EE 容器廠商都准備好了將 Servlet 2.3 規范整合到其 產品中去。對於 Servlet 2.3過濾器如何工作有一個基本的了解有助於您在設 計及編寫基於 J2EE 的應用程序時往自己的工具庫中再加入一件多功能的工具。