过滤器通过HttpServletResponseWrapper包装HttpServletResponse实现获取response中的返回数据,以及对数据进行gzip压缩
非原创文章:原文链接:http://blog.csdn.net/qq_33206732/article/details/78623042
前几天我们项目总监给了我一个任务,就是将请求的接口数据进行压缩,以达到节省流量的目的。
对于实现该功能,有以下思路:
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前加上这一句代码。试了一下,还真可以。至此问题解决。
这一次的需求,在怎么解决相应缓慢的问题花费了我一天的时间,但是也收获很很多东西。所以在这里谢谢上面代码的哥们,还有写那个虽然很短,但解决了我最终问题的博客的哥们了。
下面是两篇博客的地址:
http://blog.csdn.net/yy417168602/article/details/53534776
http://blog.csdn.net/qbian/article/details/53909778