在 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 壓縮格式的客戶端也可以收到原有的回應內容。