在Servlet容器調(diào)用某個(gè)Servlet的service()方法前,Servlet并不會(huì)知道有請(qǐng)求的到來其垄,而在Servlet的service()方法運(yùn)行之后乳讥,容器真正對(duì)瀏覽器進(jìn)行HTTP響應(yīng)之前,瀏覽器也不會(huì)知道Servlet真正的響應(yīng)是什么祸憋。過濾器正如其名稱所示吐咳,它介于Servlet之前逻悠,可攔截過濾瀏覽器對(duì)Servlet的請(qǐng)求,也可以改變Servlet對(duì)瀏覽器的響應(yīng)韭脊。本文將介紹過濾器的運(yùn)用童谒,了解如何實(shí)現(xiàn)Filter接口來編寫過濾器,以及如何使用請(qǐng)求封裝器及響應(yīng)封裝器沪羔,將容器產(chǎn)生的請(qǐng)求與響應(yīng)對(duì)象加以包裝饥伊,針對(duì)某些請(qǐng)求信息或響應(yīng)進(jìn)行加工處理。
1、過濾器的概念
想象已經(jīng)開發(fā)好應(yīng)用程序的主要商務(wù)功能了琅豆,但現(xiàn)在有幾個(gè)需求出現(xiàn):
(1)針對(duì)所有的servlet愉豺,產(chǎn)品經(jīng)理想要了解從請(qǐng)求到響應(yīng)之間的時(shí)間差。
(2)針對(duì)某些特定的頁面茫因,客戶希望只有特定的幾個(gè)用戶有權(quán)瀏覽粒氧。
(3)基于安全的考量,用戶輸入的特定字符必須過濾并替換為無害的字符节腐。
(4)請(qǐng)求與響應(yīng)的編碼從Big5改用UTF-8。
在修改源代碼之前摘盆,先分析一下這些需求:
(1)在運(yùn)行Servlet的service()方法“前”翼雀,記錄起始時(shí)間,Servlet的service()方法運(yùn)行“后”孩擂,記錄結(jié)束時(shí)間并計(jì)算時(shí)間差狼渊。
(2)在運(yùn)行Servlet的service()方法“前”,驗(yàn)證是否為允許的用戶类垦。
(3)在運(yùn)行Servlet的service()方法“前”狈邑,對(duì)請(qǐng)求參數(shù)進(jìn)行字符過濾與替換。
(4)在運(yùn)行Servlet的service()方法“前”蚤认,對(duì)請(qǐng)求與響應(yīng)對(duì)象設(shè)置編碼米苹。
經(jīng)過以上分析,可以發(fā)現(xiàn)這些需求砰琢,可以在真正運(yùn)行Servlet的service方法“前”與Servlet的service()方法“后”中間進(jìn)行實(shí)現(xiàn)蘸嘶。如下圖所示:
性能評(píng)測(cè)、用戶驗(yàn)證陪汽、字符替換训唱、編碼設(shè)置等需求,基本上與應(yīng)用程序的業(yè)務(wù)邏輯沒有直接的關(guān)系挚冤,只是應(yīng)用程序額外的元件服務(wù)之一况增。因此,這些需求應(yīng)該設(shè)計(jì)為獨(dú)立的元件训挡,使之隨時(shí)可以加入到應(yīng)用程序中澳骤,也隨時(shí)可以移除,或隨時(shí)可以修改設(shè)置而不用修改原有的業(yè)務(wù)代碼舍哄。這類元件就像是一個(gè)過濾器宴凉,安插在瀏覽器與Servlet中間,可以過濾請(qǐng)求與響應(yīng)而作進(jìn)一步的處理表悬,如下圖所示弥锄。
Servlet/JSP提供了過濾器機(jī)制讓你實(shí)現(xiàn)這些元件服務(wù),可以視 需求抽換過濾器或調(diào)整過濾器的順序,也可以針對(duì)不同的URL應(yīng)用不同的過濾器籽暇。甚至在不同的Servlet間請(qǐng)求轉(zhuǎn)發(fā)或包含時(shí)應(yīng)用過濾器温治。
2、實(shí)現(xiàn)并設(shè)置過濾器
在Servlet中要實(shí)現(xiàn)過濾器戒悠,必須實(shí)現(xiàn)Filter接口熬荆,并使用@WebFilter標(biāo)注或在web.xml中定義過濾器,讓容器知道該加載哪些過濾器類绸狐。Filter接口有三個(gè)要實(shí)現(xiàn)的方法:init()卤恳、doFilter()與destroy()。
package javax.servlet;
import java.io.IOException;
public interface Filter {
public void init(FilterConfig filterConfig) throws ServletException;
public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException;
public void destroy();
}
FilterConfig類似于Servlet接口init()方法參數(shù)上的ServletConfig寒矿,F(xiàn)ilterConfig是實(shí)現(xiàn)Filter接口的類上使用標(biāo)注或web.xml中過濾器設(shè)置信息的代表對(duì)象突琳。如果在定義過濾器時(shí)設(shè)置了初始參數(shù),則可以通過FilterConfig的getInitParameter()方法來取得初始參數(shù)符相。
Filter接口的doFilter()方法則類似于Servlet接口的service()方法拆融。當(dāng)請(qǐng)求來到容器,而容器發(fā)現(xiàn)調(diào)用Servlet的service()方法前啊终,可以應(yīng)用某過濾器時(shí)镜豹,就會(huì)調(diào)用該過濾器的doFilter()方法±渡可以在doFilter()方法中進(jìn)行service()方法的前置處理趟脂,而后決定是否調(diào)用FilterChain的doFilter()方法。如果調(diào)用了FilterChain的doFilter()方法例衍,就會(huì)運(yùn)行下一個(gè)過濾器散怖,如果沒有下一個(gè)過濾器,就調(diào)用請(qǐng)求目標(biāo)Servlet的service()方法(這里實(shí)際上用到了責(zé)任鏈模式)肄渗。如果沒有調(diào)用FilterChain的doFilter()方法镇眷,則請(qǐng)求就不會(huì)繼續(xù)交給接下來的過濾器或目標(biāo)Servlet,這就是所謂的攔截請(qǐng)求(從Servlet的角度來看翎嫡,根本不知道瀏覽器有發(fā)出請(qǐng)求)欠动。
以下是一個(gè)簡(jiǎn)單的性能評(píng)測(cè)過濾器,用來記錄請(qǐng)求與響應(yīng)的時(shí)間差惑申。
@WebFilter(
filterName="PerformanceFilter",
urlPatterns={"/*"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.ERROR,DispatcherType.ASYNC
},
initParams={@WebInitParam(name="Site", value="菜鳥教程")}
)
public class PerformanceFilter implements Filter {
private FilterConfig config;
public PerformanceFilter() {
}
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
long begin = System.currentTimeMillis();
chain.doFilter(request, response);
config.getServletContext().log("Performance process in " +
(System.currentTimeMillis() - begin) + " milliseconds");
// 輸出站點(diǎn)名稱
System.out.println("站點(diǎn)網(wǎng)址:http://www.runoob.com");
}
public void init(FilterConfig fConfig) throws ServletException {
// 獲取初始化參數(shù)
this.config = fConfig;
String site = config.getInitParameter("Site");
// 輸出初始化參數(shù)
System.out.println("PerformanceFilter init done! 網(wǎng)站名稱: " + site);
}
}
當(dāng)過濾器類被載入容器并實(shí)例化后具伍,容器會(huì)運(yùn)行其init()方法并傳入FilterConfig對(duì)象作為參數(shù)。過濾器的設(shè)置與Servlet的設(shè)置很類似圈驼,@WebFilter中的filterName設(shè)置過濾器名稱人芽,urlPatterns設(shè)置哪些URL請(qǐng)求必須應(yīng)用哪個(gè)過濾器,可應(yīng)用的URL模式與Servlet基本上相同绩脆,而”/*“表示應(yīng)用在所有的URL請(qǐng)求上萤厅。除了指定URL模式外橄抹,也可以指定Servlet名稱,這可以通過@WebFilter的servletNames來設(shè)置:
@WebFilter(filterName="PerformanceFilter", servletNames={"Servlet1","Servlet2"})
如果想一次符合所有的Servlet名稱惕味,可以使用星號(hào)(*)楼誓。如果在過濾器初始化時(shí),想要讀取一些參數(shù)名挥,可以在@WebFilter中使用@WebInitParam來設(shè)置initParams疟羹,例如:
@WebFilter(
filterName="EncodingFilter",
urlPatterns={"/encoding"},
initParams={
@WebInitParam(name="ENCODING", value="UTF-8")
})
public class EncodingFilter implements Filter {
private String ENCODING;
private FilterConfig config;
public EncodingFilter() {
}
public void init(FilterConfig fConfig) throws ServletException {
// TODO Auto-generated method stub
config = fConfig;
ENCODING = config.getInitParameter("ENCODING");
// 輸出初始化參數(shù)
System.out.println("EncodingFilter init done! ENCODING = " + ENCODING);
}
...
}
觸發(fā)過濾器的時(shí)機(jī),默認(rèn)是瀏覽器直接發(fā)出請(qǐng)求時(shí)禀倔。如果是那些通過RequestDispatcher的forward()或include()發(fā)出的請(qǐng)求榄融,需要設(shè)置@WebFilter的dispatcherTypes,例如:
@WebFilter(
filterName="some",
urlPatterns={"/some"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.ERROR,DispatcherType.ASYNC
})
如果不設(shè)置任何dispatcherTypes救湖,則默認(rèn)為REQUEST剃袍。FORWARD就是指通過RequestDispatcher的forward()方法而來的請(qǐng)求可以套用過濾器,INCLUDE是指通過RequestDispatcher的include方法而來的請(qǐng)求可以套用過濾器捎谨,ERROR是指由容器處理例外而轉(zhuǎn)發(fā)過來的請(qǐng)求可以套用過濾器,ASYNC是指異步處理器的請(qǐng)求可以觸發(fā)過濾器憔维。
3涛救、實(shí)現(xiàn)請(qǐng)求封裝器
以下通過兩個(gè)例子,來說明請(qǐng)求封裝器的實(shí)現(xiàn)與應(yīng)用业扒,分別是特殊字符替換過濾器與編碼設(shè)置過濾器检吆。
1、實(shí)現(xiàn)字符替換過濾器
假設(shè)有個(gè)留言板程序已經(jīng)上線并正常運(yùn)行中程储,但是發(fā)現(xiàn)蹭沛,有些用戶會(huì)在留言中輸入一些HTML標(biāo)簽≌吕穑基于安全性的考慮摊灭,不希望用戶輸入的HTML標(biāo)簽直接出現(xiàn)在留言中而被一些瀏覽器當(dāng)作HTML的一部分來解釋。例如败徊,并不希望用戶在留言中輸入<a href=”http://openhome.cc”>OpenHome.cc</a>這樣的信息帚呼。不希望在留言顯示中有超鏈接,希望將一些HTML字符過濾掉皱蹦,如將<煤杀、>這樣的角括號(hào)置換為HTML實(shí)體字符,可以使用過濾器的方式沪哺。但問題在于沈自,雖然可以使用HttpServletRequest的getParameter()取得請(qǐng)求參數(shù)值,但是沒有一個(gè)像setParameter()的方法辜妓,可以將處理過后的參數(shù)值重新設(shè)置給HttpServletRequest枯途。
所幸忌怎,有個(gè)HttpServletRequestWrapper幫我們實(shí)現(xiàn)了HttpServletRequest接口,只要繼承這個(gè)類柔袁,并編寫想要重新定義的方法即可呆躲。相對(duì)應(yīng)于ServletRequest接口,也有個(gè)ServletRequestWrapper類可以使用捶索。
以下范例通過繼承HttpServletRequestWrapper實(shí)現(xiàn)一個(gè)請(qǐng)求封裝器插掂,可以將請(qǐng)求參數(shù)中的HTML字符替換為HTML實(shí)體字符。
public class EscapeWrapper extends HttpServletRequestWrapper {
public EscapeWrapper(HttpServletRequest request) {
super(request);//必須調(diào)用父類構(gòu)造器腥例,將HttpServletRequest實(shí)例傳入
}
@Override
public String getParameter(String name) {
String value = getRequest().getParameter(name);
return StringEscapeUtils.escapeHtml(value);
//將請(qǐng)求參數(shù)值進(jìn)行字符替換
}
}
之后若有Servlet想取得請(qǐng)求參數(shù)值辅甥,都會(huì)調(diào)用getParameter()方法,所以這里重新定義這個(gè)方法燎竖,在此方法中璃弄,進(jìn)行字符替換動(dòng)作」够兀可以使用這個(gè)請(qǐng)求封裝器搭配過濾器夏块,以進(jìn)行字符過濾的服務(wù)。例如:
@WebFilter(
filterName="EscapeFilter",
urlPatterns={"/guestbook"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.ERROR,DispatcherType.ASYNC
})
public class EscapeFilter implements Filter {
private FilterConfig config;
public EscapeFilter() {
}
public void destroy() {
System.out.println("EscapeFilter calling done!");
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
long begin = System.currentTimeMillis();
HttpServletRequest requestWrapper = new EscapeWrapper((HttpServletRequest)request);
chain.doFilter(requestWrapper, response);
config.getServletContext().log("Request escaping HTML tags in " +
(System.currentTimeMillis() - begin) + " milliseconds");
}
public void init(FilterConfig fConfig) throws ServletException {
this.config = fConfig;
System.out.println("EscapeFilter init done!");
}
}
2纤掸、實(shí)現(xiàn)編碼設(shè)置過濾器
在之前的范例中脐供,如果要設(shè)置請(qǐng)求字符編碼,都是在個(gè)別Servlet中處理借跪≌海可以在過濾器中進(jìn)行字符編碼的統(tǒng)一設(shè)置,如果日后想要改變編碼掏愁,就不用每個(gè)Servlet逐一修改了歇由。
由于HttpServletRequest的setCharacterEncoding()方法針對(duì)的是請(qǐng)求的Body內(nèi)容,對(duì)于GET請(qǐng)求果港,必須在取得請(qǐng)求參數(shù)的字節(jié)陣列后沦泌,重新指定編碼來解析。這個(gè)需求與上一個(gè)范例類似辛掠,可搭配請(qǐng)求封裝器來實(shí)現(xiàn)赦肃。
public class EncodingWrapper extends HttpServletRequestWrapper {
private String ENCODING;
public EncodingWrapper(HttpServletRequest request, String ENCODING) {
super(request);
this.ENCODING = ENCODING;
}
@Override
public String getParameter(String name){
String value = getRequest().getParameter(name);
if(value != null) {
try {
//Web容器默認(rèn)使用ISO-8859-1編碼格式
byte[] b = value.getBytes("ISO-8859-1");
value = new String(b, ENCODING);
} catch(UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
return value;
}
}
@WebFilter(
filterName="EncodingFilter",
urlPatterns={"/encoding"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.ERROR,DispatcherType.ASYNC
},
initParams={
@WebInitParam(name="ENCODING", value="UTF-8")
})
public class EncodingFilter implements Filter {
private String ENCODING;
private FilterConfig config;
public EncodingFilter() {
}
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
if("GET".equals(req.getMethod())) {
long begin = System.currentTimeMillis();
req = new EncodingWrapper(req, ENCODING);
chain.doFilter(req, response);
config.getServletContext().log("GET Method Request Encoding process in " + (System.currentTimeMillis() - begin) + " milliseconds");
} else {
req.setCharacterEncoding(ENCODING);
chain.doFilter(req, response);
}
}
public void init(FilterConfig fConfig) throws ServletException {
config = fConfig;
ENCODING = config.getInitParameter("ENCODING");
// 輸出初始化參數(shù)
System.out.println("EncodingFilter init done! ENCODING = " + ENCODING);
}
}
請(qǐng)求參數(shù)的編碼設(shè)置是通過過濾器初始參數(shù)來設(shè)置的,并在過濾器初始化方法init()中讀取公浪,過濾器僅在GET請(qǐng)求以創(chuàng)建EncodingWrapper實(shí)例他宛,其他方法則通過HttpServletRequest的setCharacterEncoding()來設(shè)置編碼,最后都調(diào)用FilterChain的doFilter()方法傳入EncodingWrapper實(shí)例或原請(qǐng)求對(duì)象欠气。
3厅各、實(shí)現(xiàn)響應(yīng)封裝器
在Servlet中,是通過HttpServletResponse對(duì)象來對(duì)瀏覽器進(jìn)行響應(yīng)的预柒,如果想要對(duì)響應(yīng)的內(nèi)容進(jìn)行壓縮處理队塘,就要想辦法讓HttpServletResponse對(duì)象具有壓縮處理的功能袁梗。前面介紹過請(qǐng)求封裝器的實(shí)現(xiàn),而在響應(yīng)封裝器的部分憔古,可以繼承HttpServletResponseWrapper類來對(duì)HttpServletResponse對(duì)象進(jìn)行封裝遮怜。
若要對(duì)瀏覽器進(jìn)行輸出響應(yīng),必須通過getWriter()取得PrintWriter鸿市,或是通過getOutputStream()取得ServletOutputStream锯梁。 所以針對(duì)壓縮輸出的需求,主要就是繼承HttpServletResponseWrapper類之后焰情,通過重新定義這兩個(gè)方法來達(dá)成陌凳。
在下面例子中,壓縮的功能采用GZIP格式内舟,這是瀏覽器可以授受的壓縮格式合敦,可以使用GZIPOutputStream類來實(shí)現(xiàn)。由于getWriter()的PrintWriter在創(chuàng)建時(shí)验游,也是必須使用到ServletOutputStream充岛,所以在這里先擴(kuò)展ServletOutputStream類,讓它具有壓縮的功能耕蝉。
public class GZipServletOutputStream extends ServletOutputStream {
private GZIPOutputStream gzipOutputStream;
public GZipServletOutputStream(ServletOutputStream servletOutputStream) throws IOException {
this.gzipOutputStream = new GZIPOutputStream(servletOutputStream);
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener listener) {
}
public GZIPOutputStream getGzipOutputStream(){
return gzipOutputStream;
}
@Override
public void write(int b) throws IOException {
gzipOutputStream.write(b); //輸出時(shí)通過gzipOutputStream來壓縮輸出
}
}
在HttpServletResponse對(duì)象傳入Servlet的service()方法前崔梗,必須先封裝它,使得調(diào)用getOutputStream()時(shí)赔硫,可以取得這里所實(shí)現(xiàn)的GZipServletOutputStream對(duì)象,而調(diào)用getWriter()時(shí)盐肃,也可以利用GZipServletOutputStream對(duì)象來構(gòu)造PrintWriter對(duì)象爪膊。
public class CompressionWrapper extends HttpServletResponseWrapper {
private GZipServletOutputStream gzServletOutputStream;
private PrintWriter printWriter;
public CompressionWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
//響應(yīng)中已經(jīng)調(diào)用過getWriter,再調(diào)用getOutputStream就拋出異常
if(printWriter != null) {
throw new IllegalStateException();
}
if(null == gzServletOutputStream) {
gzServletOutputStream =
new GZipServletOutputStream(getResponse().getOutputStream());
}
return gzServletOutputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
//響應(yīng)中已經(jīng)調(diào)用過getOutputStream砸王,再調(diào)用getWriter就拋出異常
if(gzServletOutputStream != null) {
throw new IllegalStateException();
}
if(null == printWriter) {
gzServletOutputStream = new GZipServletOutputStream(getResponse().getOutputStream());
OutputStreamWriter osw = new OutputStreamWriter(
gzServletOutputStream, getResponse().getCharacterEncoding());
printWriter = new PrintWriter(osw);
}
return printWriter;
}
//不實(shí)現(xiàn)此方法推盛,因?yàn)檎嬲妮敵鰰?huì)被壓縮,忽略原來的內(nèi)容長(zhǎng)度設(shè)置
@Override
public void setContentLength(int len){
}
public GZIPOutputStream getGZIPOutputStream() {
if(this.gzServletOutputStream == null)
return null;
return this.gzServletOutputStream.getGzipOutputStream();
}
}
在上例中要注意谦铃,由于Servlet規(guī)范中規(guī)定耘成,在同一個(gè)請(qǐng)求期間,getWriter()與getOutputStream()只能擇一調(diào)用驹闰,否則必拋出IllegalStateException瘪菌,因此建議在實(shí)現(xiàn)響應(yīng)封裝器時(shí),也遵循這個(gè)規(guī)范嘹朗。因此在重新定義getOutputStream()與getWriter()方法時(shí)师妙,分別要檢查是否已經(jīng)存在PrintWriter與ServletOutputStream實(shí)例。
接下來就實(shí)現(xiàn)一個(gè)壓縮過濾器屹培,使用上面開發(fā)的CompressionWrapper來封裝原HttpServletResponse默穴。
@WebFilter(
filterName="CompressionFilter",
urlPatterns = { "/*" })
public class CompressionFilter implements Filter {
private FilterConfig config;
public CompressionFilter() {
}
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
String encodings = req.getHeader("accept-encoding");
//檢查是否接受壓縮
if((encodings != null) && (encodings.indexOf("gzip") > -1)) {
long begin = System.currentTimeMillis();
CompressionWrapper responseWrapper = new CompressionWrapper(res);
responseWrapper.setHeader("content-encoding", "gzip");
//設(shè)置響應(yīng)內(nèi)容編碼為gzip
chain.doFilter(request, responseWrapper);
GZIPOutputStream gzipOutputStream = responseWrapper.getGZIPOutputStream();
if(gzipOutputStream != null) {
gzipOutputStream.finish();
//調(diào)用GZIPOutputStream的finish方法完成壓縮輸出
}
config.getServletContext().log("gzip compression process in " +
(System.currentTimeMillis() - begin) + " milliseconds");
}
else {
chain.doFilter(request, response);
//不接受壓縮直接進(jìn)行下一個(gè)過濾器
}
}
public void init(FilterConfig fConfig) throws ServletException {
this.config = fConfig;
System.out.println("CompressionFilter init done!");
}
}
瀏覽器是否接受GZIP壓縮格式怔檩,可以通過檢查accept-encoding請(qǐng)求標(biāo)頭中是否包括gzip字符串來判斷。如果可以接受GZIP壓縮蓄诽,創(chuàng)建CompressionWrapper封裝原響應(yīng)對(duì)象薛训,并設(shè)置content-encoding響應(yīng)標(biāo)頭為gzip,這樣瀏覽器就會(huì)知道響應(yīng)內(nèi)容是GZIP壓縮格式仑氛。接著調(diào)用FilterChain的doFilter()時(shí)乙埃,傳入響應(yīng)對(duì)象為CompressionWrapper對(duì)象。當(dāng)FilterChain的doFilter()結(jié)束時(shí)调衰,必須調(diào)用GZIPOutputStream的finish()方法膊爪,這才會(huì)將GZIP后的資料從緩沖區(qū)全部移出并進(jìn)行響應(yīng)。
如果瀏覽器不接受GZIP壓縮格式嚎莉,則直接調(diào)用FilterChain的doFilter()米酬,這樣就可以讓不接受GZIP壓縮格式的客戶端也可以收到原有的響應(yīng)內(nèi)容。