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

