1. 程式人生 > >過濾器通過HttpServletResponseWrapper包裝HttpServletResponse實現獲取response中的返回資料,以及對資料進行gzip壓縮

過濾器通過HttpServletResponseWrapper包裝HttpServletResponse實現獲取response中的返回資料,以及對資料進行gzip壓縮

前幾天我們專案總監給了我一個任務,就是將請求的介面資料進行壓縮,以達到節省流量的目的。

對於實現該功能,有以下思路:

1.獲取到response中的值,
2.對資料進行gzip壓縮(因為要求前端不變,所以只能選在這個瀏覽器都支援的壓縮方式)
3.將資料寫入到response中,
4.將response返貨前端

但是,當我執行第一步的時候,就遇到了很蛋疼的事情,response中的返回資料拿不到,這裡就很無語了,又不允許在每個介面方法都加上處理方法,剛開始想的是在攔截器中的afterCompletion()方法裡進行資料處理的,但是response裡沒有提供可以獲取body值的方法,只能自己想辦法了。

通過網上查詢,有一種方式可以獲取到response中的資料,就是使用HttpServletResponseWrapper包裝HttpServletResponse來實現。

通過網上找通過HttpServletResponseWrapper實現獲取response中的資料,大概有兩個版本,有一個版本的數量很多,但是根本沒用啊,就是下面的程式碼:

public class ResponseWrapper extends HttpServletResponseWrapper {
    private PrintWriter cachedWriter;
    private CharArrayWriter bufferedWriter;

    public
ResponseWrapper(HttpServletResponse response) throws IOException { super(response); bufferedWriter = new CharArrayWriter(); cachedWriter = new PrintWriter(bufferedWriter); } public PrintWriter getWriter() throws IOException { return cachedWriter; } public
String getResult() { byte[] bytes = bufferedWriter.toString().getBytes(); try { return new String(bytes, "UTF-8"); } catch (Exception e) { LoggerUtil.logError(this.getClass().getName(), "getResult", e); return ""; } } }

經過測試getResult()根本就獲取不到值,具體的大家可以研究下上面的程式碼,就知道為啥了,完全是一個坑啊,這裡就不多說了。

還有另一個版本,也就是我現在用的(這裡先謝謝這位哥們了,具體的原路徑一會貼在下面),下面是我的程式碼
原來的程式碼在我這裡有一個問題,不知道是都有這個問題,還是就我這有問題,下面會說什麼問題以及怎麼解決的

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;

public class ResponseWrapper extends HttpServletResponseWrapper {

    private ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    private HttpServletResponse response;
    private PrintWriter pwrite;

    public ResponseWrapper(HttpServletResponse response) {
        super(response);
        this.response = response;
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new MyServletOutputStream(bytes); // 將資料寫到 byte 中
    }

    /**
     * 重寫父類的 getWriter() 方法,將響應資料快取在 PrintWriter 中
     */
    @Override
    public PrintWriter getWriter() throws IOException {
        try{
            pwrite = new PrintWriter(new OutputStreamWriter(bytes, "utf-8"));
        } catch(UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return pwrite;
    }

    /**
     * 獲取快取在 PrintWriter 中的響應資料
     * @return
     */
    public byte[] getBytes() {
        if(null != pwrite) {
            pwrite.close();
            return bytes.toByteArray();
        }

        if(null != bytes) {
            try {
                bytes.flush();
            } catch(IOException e) {
                e.printStackTrace();
            }
        }
        return bytes.toByteArray();
    }

    class MyServletOutputStream extends ServletOutputStream {
        private ByteArrayOutputStream ostream ;

        public MyServletOutputStream(ByteArrayOutputStream ostream) {
            this.ostream = ostream;
        }

        @Override
        public void write(int b) throws IOException {
            ostream.write(b); // 將資料寫到 stream 中
        }

    }

}

因為HttpServletResponse的包裝類只能在過濾器中使用,所以只能在過濾器中實現了,下面是我的過濾器的doFilter()方法的程式碼:

 @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String headEncoding = ((HttpServletRequest)servletRequest).getHeader("accept-encoding");
        if (headEncoding == null || (headEncoding.indexOf("gzip") == -1)) { // 客戶端 不支援 gzip
            filterChain.doFilter(servletRequest, servletResponse);
            System.out.println("----------------該瀏覽器不支援gzip格式編碼-----------------");
        } else { // 支援 gzip 壓縮,對資料進行gzip壓縮

            HttpServletRequest req = (HttpServletRequest) servletRequest;
            HttpServletResponse resp = (HttpServletResponse) servletResponse;
            ResponseWrapper mResp = new ResponseWrapper(resp); // 包裝響應物件 resp 並快取響應資料

            filterChain.doFilter(req, mResp);

            byte[] bytes = mResp.getBytes(); // 獲取快取的響應資料
            System.out.println("壓縮前大小:" + bytes.length);
            System.out.println("壓縮前資料:" + new String(bytes,"utf-8"));

            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            GZIPOutputStream gzipOut = new GZIPOutputStream(bout); // 建立 GZIPOutputStream 物件

            gzipOut.write(bytes); // 將響應的資料寫到 Gzip 壓縮流中
            gzipOut.flush();
            gzipOut.close(); // 將資料重新整理到  bout 位元組流陣列

            byte[] bts = bout.toByteArray();
            System.out.println("壓縮後大小:" + bts.length);
            resp.setHeader("Content-Encoding", "gzip"); // 設定響應頭資訊
            resp.getOutputStream().write(bts); // 將壓縮資料響應給客戶端
        }
    }

這裡我解釋下上面的程式碼,首先判斷一下request請求接不接受gzip壓縮,這個是根據request的請求頭的accept-encoding這個屬性來判斷,因為現在的各大瀏覽器都是支援gzip的,所以如果你想做gzip壓縮,前端只需要加上這個請求頭,如果後端返回的資料是gzip壓縮過的資料,瀏覽器就會自動解壓的。

上面的程式碼
如果不支援gzip壓縮,不處理,正常流程往下走。
如果支援gzip壓縮,就需要資料處理

大家可以看下這個程式碼

filterChain.doFilter(req, mResp);

這個方法很重要,這個方法前面部分都是請求介面之前的部分,如果你有一些想要在呼叫介面前統一處理的東西都可以在前面處理,當然你也可以在攔截器的preHandle()方法中處理。對應的這個方法之後的部分就是請求介面有返回值之後的部分了。也就是這次我們需要進行對資料壓縮的部分。

當然需要注意的是doFilter的第二個引數,原本是ServletResponse物件的,但是現在因為要處理資料,我們使用ResponseWrapper類包裝了ServletResponse,所以第二個引數傳的就是ResponseWrapper物件了,當然對應的如果你包裝了servletRequest,那麼第一個引數就要傳你包裝servletRequest類的物件了。

接下來就是先用包裝類物件獲取返回的資料,然後使用GZIPOutputStream對資料進行壓縮,然後在使用resp.getOutputStream().write(bts); 將壓縮後的資料寫入到response中,當然,我們不能忘了需要在返回的請求頭加上Content-Encoding(返回內容編碼)為gzip格式。

這樣我們就可以將response中的資料拿出來進行壓縮後返回到前端,當然你不一定要壓縮,你也可以加密等等處理。

在上面的流程中,我遇到了一個問題,需要注意一下,不知道你們有沒有遇到,
就是上面的流程進行的都很正常,資料也獲取到了,壓縮也壓縮了,執行時間也打印出來了,但是前端一直在響應中,也就是說我們響應的太慢了,我看了下,平均在30秒左右,這就沒有辦法接受了。

剛開始我以為是前端對gzip資料解壓的速度太慢,但是我遮蔽掉gzip相關程式碼,返顯資料返回的還是一樣的慢,所以gzip壓縮解壓排除。

然後只能是一個地方有問題了,那就是我們的包裝類ResponseWrapper有問題了,通過debug,我發現我們封裝的類中的各個方法執行的順序,

首先在我們new 一個物件的時候呼叫了它的構造方法ResponseWrapper(HttpServletResponse response)方法,然後在執行過濾器的doFilter方法的時候,會呼叫包裝類的getOutputStream()方法將資料寫入到我們定義的ByteArrayOutputStream中 也就是bytes 中,然後我們呼叫getBytes()方法將bytes轉換成byte陣列返回,這裡面就是我們的返回資料。

我們從上面的流程中可以看到,理論上沒有問題,實際上我們也獲取到了我們想要的資料,這些方法執行速度也很快,沒有在哪部分卡頓住。那問題出現在哪呢,我從網上搜了半天,這方面的資料很少,最後在一個部落格中,寫了這一句程式碼就是在寫資料之前我們需要使用Response物件充值contentLength。也就是下面這一句程式碼

response.setContentLength(-1);

這裡我剛開始沒有想到在哪加這一段程式碼,本來想的是在過濾器中,但是想了想,加入的時機都不對,後來看看包裝類,發現了寫這個程式碼的哥們定義了一個HttpServletResponse物件,並且在構造方法中也初始化了。但是全文沒有用到這個response物件。我就想是不是在我們執行方法是呼叫getOutputStream()將資料寫入到bytes前加上這一句程式碼。試了一下,還真可以。至此問題解決。

這一次的需求,在怎麼解決相應緩慢的問題花費了我一天的時間,但是也收穫很很多東西。所以在這裡謝謝上面程式碼的哥們,還有寫那個雖然很短,但解決了我最終問題的部落格的哥們了。