在 Servle t中,是透過 HttpServletResponse 物件來對瀏覽器進行回應,如果你想要對回應的內容進行壓縮處理,就要想辦法讓 HttpServletResponse 物件具有壓縮處理的功能。先前介紹過請求包裹器的實作,而在回應包裹器的部份,你可以繼承 HttpServletResponseWrapper 類別(父類別 ServletResponseWrapper)來對 HttpServletResponse 物件進行包裹。
若要對瀏覽器進行輸出回應必須透過 getWriter() 取得 PrintWriter,或是透過 getOutputStream() 取得 ServletOutputStream。所以針對壓縮輸出的需求,主要就是繼承 HttpServletResponseWrapper 之後,透過重新定義這兩個方法來達成。
在這邊壓縮的功能將採 GZIP 格式,這是瀏覽器可以接受的壓縮格式,可以使用 GZIPOutputStream 類別來實作。由於 getWriter() 的 PrintWriter 在建立時,也是必須使用到 ServletOutputStream,所以在這邊先擴充 ServletOutputStream 類別,讓它具有壓縮的功能。
package cc.openhome;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
public class GZipServletOutputStream extends ServletOutputStream {
private ServletOutputStream servletOutputStream;
private GZIPOutputStream gzipOutputStream;
public GZipServletOutputStream(
ServletOutputStream servletOutputStream) throws IOException {
this.servletOutputStream = servletOutputStream;
this.gzipOutputStream = new GZIPOutputStream(servletOutputStream);
}
public void write(int b) throws IOException {
this.gzipOutputStream.write(b);
}
public GZIPOutputStream getGzipOutputStream() {
return this.gzipOutputStream;
}
@Override
public boolean isReady() {
return this.servletOutputStream.isReady();
}
@Override
public void setWriteListener(WriteListener writeListener) {
this.servletOutputStream.setWriteListener(writeListener);
}
@Override
public void close() throws IOException {
this.gzipOutputStream.close();
}
@Override
public void flush() throws IOException {
this.gzipOutputStream.flush();
}
public void finish() throws IOException {
this.gzipOutputStream.finish();
}
}
GzipServletOutputStream 繼承 ServletOutputStream 類別,使用時必須傳入 ServletOutputStream 類別,由 GZIPOutputStream 來增加壓縮輸出串流的功能。範例中重新定義 write() 方法,並透過 GZIPOutputStream 的 write() 方法來作串流輸出,GZIPOutputStream 的 write() 方法 實作了壓縮的功能。
在 HttpServletResponse 物件傳入 Servlet 的 service() 方法前,必須包裹它,使得呼叫 getOutputStream() 時,可以使用取得這邊所實作的 GzipServletOutputStream 物件,而呼叫 getWriter() 時,也可以利用 GzipServletOutputStream 物件 來建構 PrintWriter 物件。
package cc.openhome;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class CompressionResponseWrapper extends HttpServletResponseWrapper {
private GZipServletOutputStream gzServletOutputStream;
private PrintWriter printWriter;
public CompressionResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if(printWriter != null) {
throw new IllegalStateException();
}
if (gzServletOutputStream == null) {
gzServletOutputStream =
new GZipServletOutputStream(getResponse().getOutputStream());
}
return gzServletOutputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
if(gzServletOutputStream != null) {
throw new IllegalStateException();
}
if (printWriter == null) {
gzServletOutputStream =
new GZipServletOutputStream(getResponse().getOutputStream());
OutputStreamWriter osw =
new OutputStreamWriter(
gzServletOutputStream, getResponse().getCharacterEncoding());
printWriter = new PrintWriter(osw);
}
return printWriter;
}
@Override
public void flushBuffer() throws IOException {
if(this.printWriter != null) {
this.printWriter.flush();
}
else if(this.gzServletOutputStream != null) {
this.gzServletOutputStream.flush();
}
super.flushBuffer();
}
public void finish() throws IOException {
if(this.printWriter != null) {
this.printWriter.close();
}
else if(this.gzServletOutputStream != null) {
this.gzServletOutputStream.finish();
}
}
@Override
public void setContentLength(int len) {}
@Override
public void setContentLengthLong(long length) {}
}
在上例中要注意,由於 Servlet 規格書中規定,在同一個請求期間,getWriter() 與 getOutputStream() 只能擇一呼叫,否則必須丟出 IllegalStateException,因此建議在實作回應包裹器時,也遵循這個規範,因此在重新定義 getOutputStream() 與 getWriter() 方法時,分別要檢查是否已存在 PrintWriter 與 ServletOutputStream 實例。
在 getOutputStream() 中建立 GZipServletOutputStream 實例並傳回。在 getWriter() 中呼叫 getOutputStream() 取得 GZipServletOutputStream 物件,作為建構 PrintWriter 實例時使用,如此所建立的 PrintWriter 物件也就具有壓縮功能。由於真正的輸出會被壓縮,忽略原來的內容長度設定。
接下來可以實作一個壓縮過濾器,使用上面所開發的 CompressionResponseWrapper 來包裹原 HttpServletResponse。
package cc.openhome;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.WebFilter;
@WebFilter("/*")
public class CompressionFilter extends HttpFilter {
protected void doFilter(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String encodings = request.getHeader("Accept-Encoding");
if (encodings != null && encodings.contains("gzip")) {
CompressionResponseWrapper responseWrapper =
new CompressionResponseWrapper(response);
responseWrapper.setHeader("Content-Encoding", "gzip");
chain.doFilter(request, responseWrapper);
responseWrapper.finish();
}
else {
chain.doFilter(request, response);
}
}
}
瀏覽器是否接受 GZIP 壓縮格式,可以透過檢查 Accept-Encoding 請求標頭中是否包括 "gzip" 字串來判斷。如果可以接受 GZIP 壓縮,建立 CompressionResponseWrapper 包裹原回應物件,並設定 Content-Encoding 回應標頭為 "gzip",如此瀏覽器就會知道回應內容是 GZIP 壓縮格式。
接著呼叫 FilterChain 的 doFilter() 時,傳入的回應物件為 CompressionResponseWrapper 物件。當 FilterChain 的 doFilter() 結束時,必須呼叫 GZIPOutputStream 的 finish() 方法,這才會將 GZIP 後的資料從緩衝區中全部移出並進行回應,這實作在 CompressionResponseWrapper 的 finish() 方法中。
如果客戶端不接受 GZIP 壓縮格式,則直接呼叫 FilterChain 的 doFilter(),這樣就可以讓不接受 GZIP 壓縮格式的客戶端也可以收到原有的回應內容。

