过滤器通过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

posted @ 2018-06-01 16:48  喻聪  阅读(5223)  评论(0编辑  收藏  举报