过滤器通过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 ""; } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

经过测试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 中 } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

因为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); // 将压缩数据响应给客户端 } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

这里我解释下上面的代码,首先判断一下request请求接不接受gzip压缩,这个是根据request的请求头的accept-encoding这个属性来判断,因为现在的各大浏览器都是支持gzip的,所以如果你想做gzip压缩,前端只需要加上这个请求头,如果后端返回的数据是gzip压缩过的数据,浏览器就会自动解压的。

上面的代码 
如果不支持gzip压缩,不处理,正常流程往下走。 
如果支持gzip压缩,就需要数据处理

大家可以看下这个代码

filterChain.doFilter(req, mResp);
  • 1

这个方法很重要,这个方法前面部分都是请求接口之前的部分,如果你有一些想要在调用接口前统一处理的东西都可以在前面处理,当然你也可以在拦截器的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);
  • 1

这里我刚开始没有想到在哪加这一段代码,本来想的是在过滤器中,但是想了想,加入的时机都不对,后来看看包装类,发现了写这个代码的哥们定义了一个HttpServletResponse对象,并且在构造方法中也初始化了。但是全文没有用到这个response对象。我就想是不是在我们执行方法是调用getOutputStream()将数据写入到bytes前加上这一句代码。试了一下,还真可以。至此问题解决。

这一次的需求,在怎么解决相应缓慢的问题花费了我一天的时间,但是也收获很很多东西。所以在这里谢谢上面代码的哥们,还有写那个虽然很短,但解决了我最终问题的博客的哥们了。

下面是两篇博客的地址: 
http://blog.csdn.net/yy417168602/article/details/53534776 
http://blog.csdn.net/qbian/article/details/53909778

猜你喜欢

转载自www.cnblogs.com/yucongblog/p/9122409.html