Servlet 2.3過濾器程式設計
摘要
Jason Hunter通過對一些自由而又實用的過濾器的研究以對新的servlet過濾器模型進行深入探討。你將知道這些過濾器是如何工作以及你能用他們做什麼。最後,Jason介紹了他自己為簡化檔案上傳而做的多路請求過濾器。
在"Servlet 2.3: New Features Exposed,"中,我介紹了Servlet API 2.3中的變化並給出了一個簡單的servlet過濾器模型。在隨後的文章中,我將對servlet過濾器進行深入的挖掘,而你看到的這些servlet過濾器都是能從Web上免費下載的。對每一個過濾器,我將檢視它是做什麼的,如何工作的,以及你能從哪裡得到它。
你可以在兩種情況下使用本文:學習過濾器的功用,或者作為你寫過濾器時的輔助。我將從幾個簡單的例子開始然後繼續更多高階的過濾器。最後,我將向你介紹我為了支援多路請求而寫的一個檔案上傳過濾器。
Servlet 過濾器
也許你還不熟悉情況,一個過濾器是一個可以傳送請求或修改響應的物件。過濾器並不是servlet,他們並不實際建立一個請求。他們是請求到達一個servlet前的預處理程式,和/或響應離開servlet後的後處理程式。就像你將在後面的例子中看到的,一個過濾器能夠:
·在一個servlet被呼叫前截獲該呼叫
·在一個servlet被呼叫前檢查請求
·修改在實際請求中提供了可定製請求物件的請求頭和請求資料
·修改在實際響應中提供了可定製響應物件的響應頭和響應資料
·在一個servlet被呼叫之後截獲該呼叫
你可以一個過濾器以作用於一個或一組servlet,零個或多個過濾器能過濾一個或多個servlet。一個過濾器實現java.servlet.Filter介面並定義它的三個方法:
1. void init(FilterConfig config) throws ServletException:在過濾器執行service前被呼叫,以設定過濾器的配置物件。
2. void destroy();在過濾器執行service後被呼叫。
3. Void doFilter(ServletRequest req,ServletResponse res,FilterChain chain) throws IOException,ServletException;執行實際的過濾工作。
伺服器呼叫一次init(FilterConfig)以為服務準備過濾器,然後在請求需要使用過濾器的任何時候呼叫doFilter()。FilterConfig介面檢索過濾器名、初始化引數以及活動的servlet上下文。伺服器呼叫destory()以指出過濾器已結束服務。過濾器的生命週期和servelt的生命週期非常相似 ——在Servlet API 2.3 最終釋出稿2號 中最近改變的。先前得用setFilterConfig(FilterConfig)方法來設定生命週期。
在doFilter()方法中,每個過濾器都接受當前的請求和響應,而FilterChain包含的過濾器則仍然必須被處理。doFilter()方法中,過濾器可以對請求和響應做它想做的一切。(就如我將在後面討論的那樣,通過呼叫他們的方法收集資料,或者給物件新增新的行為。)過濾器呼叫
chain.doFilter()將控制權傳送給下一個過濾器。當這個呼叫返回後,過濾器可以在它的doFilter()方法的最後對響應做些其他的工作;例如,它能記錄響應的資訊。如果過濾器想要終止請求的處理或或得對響應的完全控制,則他可以不呼叫下一個過濾器。
循序漸進
如果想要真正理解過濾器,則應該看它們在實際中的應用。我們將看到的第一個過濾器是簡單而有用的,它記錄了所有請求的持續時間。在Tomcat 4.0釋出中被命名為ExampleFilter。程式碼如下:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class TimerFilter implements Filter {
private FilterConfig config = null;
public void init(FilterConfig config) throws ServletException {
this.config = config;
}
public void destroy() {
config = null;
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
long before = System.currentTimeMillis();
chain.doFilter(request, response);
long after = System.currentTimeMillis();
String name = "";
if (request instanceof HttpServletRequest) {
name = ((HttpServletRequest)request).getRequestURI();
}
config.getServletContext().log(name + ": " + (after - before) + "ms");
}
}
當伺服器呼叫init()時,過濾器用config變數來儲存配置類的引用,這將在後面的doFilter()方法中被使用以更改ServletContext。當呼叫doFilter()時,過濾器計算請求發生到該請求執行完畢之間的時間。該過濾器很好的演示了請求之前和之後的處理。注意doFilter()方法的引數並不是HTTP物件,因此要呼叫HTTP專用的getRequestURI()方法時必須將request轉化為HttpServletRequest型別。
使用此過濾器,你還必須在web.xml檔案中用<filter>標籤部署它,見下:
<filter>
<filter-name>timerFilter</filter-name>
<filter-class>TimerFilter</filter-class>
</filter>
這將通知伺服器一個叫timerFiter的過濾器是從TimerFiter類實現的。你可以使用確定的URL模式或使用<filter-mapping>標籤命名的servelt 來註冊一個過濾器,如:
<filter-mapping>
<filter-name>timerFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
這種配置使過濾器操作所有對伺服器的請求(靜態或動態),正是我們需要的計時過濾器。如果你連線一個簡單的頁面,記錄輸出可能如下:
2001-05-25 00:14:11 /timer/index.html: 10ms
在Tomcat 4.0 beta 5中,你可以在server_root/logs/下找到該記錄檔案。
此過濾器的WAR檔案從此下載:
http://www.javaworld.com/jw-06-2001/Filters/timer.war
誰在你的網站上?他們在做什麼?
我們下一個過濾器是由OpenSymphony成員寫的clickstream過濾器。這個過濾器跟蹤使用者請求(比如:點選)和請求佇列(比如:點選流)以向網路管理員顯示誰在她的網站上以及每個使用者正在訪問那個頁面。這是個使用LGPL的開源庫。
在clickstream包中你將發現一個捕獲請求資訊的ClickstreamFilter類,一個像操作結構一樣的Clickstream類以儲存資料,以及一個儲存會話和上下文事件的ClickstreamLogger類以將所有東西組合在一起。還有個BotChecker類用來確定客戶端是否是一個機器人(簡單的邏輯,像“他們是否是從robots.txt來的請求?”)。該包中提供了一個clickstreams.jsp摘要頁面和一個viewstream.jsp詳細頁面來檢視資料。
我們先看ClickstreamFilter類。所有的這些例子都做了些輕微的修改以格式化並修改了些可移植性問題,這我將在後面將到。
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.*;
public class ClickstreamFilter implements Filter {
protected FilterConfig filterConfig;
private final static String FILTER_APPLIED = "_clickstream_filter_applied";
public void init(FilterConfig config) throws ServletException {
this.filterConfig = filterConfig;
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 確保該過濾器在每次請求中只被使用一次
if (request.getAttribute(FILTER_APPLIED) == null) {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
HttpSession session = ((HttpServletRequest)request).getSession();
Clickstream stream = (Clickstream)session.getAttribute("clickstream");
stream.addRequest(((HttpServletRequest)request));
}
// 傳遞請求
chain.doFilter(request, response);
}
public void destroy() { }
}
doFilter()方法取得使用者的session,從中獲取Clickstream,並將當前請求資料加到Clickstream中。其中使用了一個特殊的FILTER_APPLIED標記屬性來標註此過濾器是否已經被當前請求使用(可能會在請求排程中發生)並且忽略所有其他的過濾器行為。你可能疑惑過濾器是怎麼知道當前session中有clickstream屬性。那是因為ClickstreamLogger在會話一開始時就已經設定了它。ClickstreamLogger程式碼:
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class ClickstreamLogger implements ServletContextListener,
HttpSessionListener {
Map clickstreams = new HashMap();
public ClickstreamLogger() { }
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext().setAttribute("clickstreams", clickstreams);
}
public void contextDestroyed(ServletContextEvent sce) {
sce.getServletContext().setAttribute("clickstreams", null);
}
public void sessionCreated(HttpSessionEvent hse) {
HttpSession session = hse.getSession();
Clickstream clickstream = new Clickstream();
session.setAttribute("clickstream", clickstream);
clickstreams.put(session.getId(), clickstream);
}
public void sessionDestroyed(HttpSessionEvent hse) {
HttpSession session = hse.getSession();
Clickstream stream = (Clickstream)session.getAttribute("clickstream");
clickstreams.remove(session.getId());
}
}
logger(記錄器)獲取應用事件並將使用他們將所有東西幫定在一起。當context建立中,logger在context中放置了一個共享的流map。這使得clickstream.jsp頁面知道當前活動的是哪個流。而在context銷燬中,logger則移除此map。當一個新訪問者建立一個新的會話時,logger將一個新的Clickstream例項放入此會話中並將此Clickstream加入到中心流map中。在會話銷燬時,由logger從中心map中移除這個流。
下面的web.xml部署描述片段將所有東西寫在一塊:
<filter>
<filter-name>clickstreamFilter</filter-name>
<filter-class>ClickstreamFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>clickstreamFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>clickstreamFilter</filter-name>
<url-pattern>*.html</url-pattern>
</filter-mapping>
<listener>
<listener-class>ClickstreamLogger</listener-class>
</listener>
這注冊了ClickstreamFilter並設定其處理*.jsp和*.html來的請求。這也將ClickstreamLogger註冊為一個監聽器以在應用事件發生時接受他們。
兩個JSP頁面從會話中取clickstream資料和context物件並使用HTML介面來顯示當前狀態。下面的clickstream.jsp檔案顯示了個大概:
<%@ page import="java.util.*" %>
<%@ page import="Clickstream" %>
<%
Map clickstreams = (Map)application.getAttribute("clickstreams");
String showbots = "false";
if (request.getParameter("showbots") != null) {
if (request.getParameter("showbots").equals("true"))
showbots = "true";
else if (request.getParameter("showbots").equals("both"))
showbots = "both";
}
%>
<font face="Verdana" size="-1">
<h1>All Clickstreams</h1>
<a href="clickstreams.jsp?showbots=false">No Bots</a> |
<a href="clickstreams.jsp?showbots=true">All Bots</a> |
<a href="clickstreams.jsp?showbots=both">Both</a> <p>
<% if (clickstreams.keySet().size() == 0) { %>
No clickstreams in progress
<% } %>
<%
Iterator it = clickstreams.keySet().iterator();
int count = 0;
while (it.hasNext()) {
String key = (String)it.next();
Clickstream stream = (Clickstream)clickstreams.get(key);
if (showbots.equals("false") && stream.isBot()) {
continue;
}
else if (showbots.equals("true") && !stream.isBot()) {
continue;
}
count++;
try {
%>
<%= count %>.
<a href="viewstream.jsp?sid=<%= key %>"><b>
<%= (stream.getHostname() != null && !stream.getHostname().equals("") ?
stream.getHostname() : "Stream") %>
</b></a> <font size="-1">[<%= stream.getStream().size() %> reqs]</font><br>
<%
}
catch (Exception e) {
%>
An error occurred - <%= e %><br>
<%
}
}
%>
這個包很容易從OpenSymphony下載並安裝。將Java檔案編譯並放在
WEB-INF/classes下,將JSP檔案放到Web應用路徑下,按幫助修改web.xml檔案。為防止在這些工作前的爭論,你可以從
http://www.javaworld.com/jw-06-2001/Filters/clickstream.war處找到打好包的WAR檔案。
為能讓此過濾器能在Tomcat 4.0 beta 5下工作,我發現我不得不做一些輕微的改動。我做的改動顯示了一些在servlet和過濾器的可移植性中通常容易犯的錯誤,所以我將他們列在下面:
·我不得不將在JSP中新增一個額外的匯入語句:<%@ page import=”Clickstream” %>。在Java中你並不需要匯入在同一包下的類,而在伺服器上JSP被編譯到預設包中,你並不需要這句匯入行。但在像Tomcat這樣的伺服器上,JSP被編譯到一個自定義的包中,你不得不明確地從預設包中匯入類。
·我不得不將<listener>元素移動到web.xml檔案中的<filter>和<filter-mapping>元素之後,就像部署描述DTD要求的那樣。並不是所有伺服器對元素都要求固定的順序。但Tomcat必須要。
·我不得不將web.xml中的對映由/*.html和/*.jsp改成正確的*.html和*.jsp。一些伺服器會忽略開頭的/,但Tomcat強硬的規定開頭不能有/。
·最後,我得將ClickstreamFilter類升級到最新的生命週期API,將setFilterConfig()改成新的init()和destory()方法。
可下載的WAR檔案已經包含了這些修改並能通過伺服器在包外執行,雖然我並沒有廣泛的進行測試。
壓縮響應
第三個過濾器是自動壓縮響應輸出流,以提高頻寬利用率並提供一個很好的包裝響應物件的示例。這個過濾器是由來自SUN的Amy Roh編寫的,他為Tomcat 4.0 的“examples”Web程式做出過貢獻。你將從webapps/examples/WEB-INF/classes/compressionFilters下找到原始程式碼。這裡的例子程式碼以及WAR下的都已經為了更清晰和更簡單而編輯過了。
CompressionFilter類的策略是檢查請求頭以判定客戶端是否支援壓縮,如果支援,則將響應物件用自定義的響應來打包,它的getOutputStream()和getWriter()方法已經被定義為可以利用壓縮過的輸出流。使用過濾器允許如此簡單而有效的解決問題。
我們將從init()開始看程式碼:
public void init(FilterConfig filterConfig) {
config = filterConfig;
compressionThreshold = 0;
if (filterConfig != null) {
String str = filterConfig.getInitParameter("compressionThreshold");
if (str != null) {
compressionThreshold = Integer.parseInt(str);
}
else {
compressionThreshold = 0;
}
}
}
注意在檢索請求頭前必須把request轉化為HttpServletRequest,就想在第一個例子裡那樣。過濾器使用wrapper類CompressResponseWrapper,一個從
HttpServletResponseWrapper類繼承下來的自定義類。這個wrapper的程式碼相對比較簡單:
public class CompressionResponseWrapper extends HttpServletResponseWrapper {
protected ServletOutputStream stream = null;
protected PrintWriter writer = null;
protected int threshold = 0;
protected HttpServletResponse origResponse = null;
public CompressionResponseWrapper(HttpServletResponse response) {
super(response);
origResponse = response;
}
public void setCompressionThreshold(int threshold) {
this.threshold = threshold;
}
public ServletOutputStream createOutputStream() throws IOException {
return (new CompressionResponseStream(origResponse));
}
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been " +
"called for this response");
}
if (stream == null) {
stream = createOutputStream();
}
((CompressionResponseStream) stream).setCommit(true);
((CompressionResponseStream) stream).setBuffer(threshold);
return stream;
}
public PrintWriter getWriter() throws IOException {
if (writer != null) {
return writer;
}
if (stream != null) {
throw new IllegalStateException("getOutputStream() has already " +
"been called for this response");
}
stream = createOutputStream();
((CompressionResponseStream) stream).setCommit(true);
((CompressionResponseStream) stream).setBuffer(threshold);
writer = new PrintWriter(stream);
return writer;
}
}
所有呼叫getOutputStream() 或者getWriter()都返回一個使用
CompressResponseStream類的物件。CompressionResponseStrteam類沒有顯示在這個例子中,因為它繼承於ServletOutputStream並使用java.util.zip.GZIPOutputStream類來壓縮流。
Tomcat的”examples”Web程式中已經預先配置了這個壓縮過濾器並載入了一個示例servlet。示例servlet響應/CompressionTestURL(確定先前的路徑是/examples)。使用我製作的有用的WAR檔案,你可以用/servlet/compressionTest(再次提醒,別忘了適當的前導路徑)訪問此測試servlet。你可以使用如下的web.xml片段來配置這個測試:
<filter>
<filter-name>compressionFilter</filter-name>
<filter-class>CompressionFilter</filter-class>
<init-param>
<param-name>compressionThreshold</param-name>
<param-value>10</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>compressionFilter</filter-name>
<servlet-name>compressionTest</servlet-name>
</filter-mapping>
<servlet>
<servlet-name>
compressionTest
</servlet-name>
<servlet-class>
CompressionTestServlet
</servlet-class>
</servlet>
CompressionTestServlet(這裡沒有顯示)輸出壓縮是否可用,如果可用,則輸出壓縮響應成功!