回應包裹器


在 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() 方法,並透過 GZIPOutputStreamwrite() 方法來作串流輸出,GZIPOutputStreamwrite() 方法 實作了壓縮的功能。

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() 方法時,分別要檢查是否已存在 PrintWriterServletOutputStream 實例。

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 壓縮格式。

接著呼叫 FilterChaindoFilter() 時,傳入的回應物件為 CompressionResponseWrapper 物件。當 FilterChaindoFilter() 結束時,必須呼叫 GZIPOutputStreamfinish() 方法,這才會將 GZIP 後的資料從緩衝區中全部移出並進行回應,這實作在 CompressionResponseWrapperfinish() 方法中。

如果客戶端不接受 GZIP 壓縮格式,則直接呼叫 FilterChaindoFilter(),這樣就可以讓不接受 GZIP 壓縮格式的客戶端也可以收到原有的回應內容。