過濾器通過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前加上這一句程式碼。試了一下,還真可以。至此問題解決。
這一次的需求,在怎麼解決相應緩慢的問題花費了我一天的時間,但是也收穫很很多東西。所以在這裡謝謝上面程式碼的哥們,還有寫那個雖然很短,但解決了我最終問題的部落格的哥們了。