过滤器通过HttpServletResponseWrapper包装HttpServletResponse实现获取response中的返回数据,以及对数据进行gzip压缩

来源:互联网 发布:3dsmax 显卡优化驱动 编辑:程序博客网 时间:2024/06/06 03:02

前几天我们项目总监给了我一个任务,就是将请求的接口数据进行压缩,以达到节省流量的目的。

对于实现该功能,有以下思路:

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

阅读全文
0 0
原创粉丝点击