Filter高级开发(三)——压缩响应正文内容

来源:互联网 发布:医疗器械国家数据库 编辑:程序博客网 时间:2024/06/06 14:13

使用Decorator设计模式增强response对象

Servlet API中提供了response对象的Decorator设计模式的默认实现类HttpServletResponseWrapper,HttpServletResponseWrapper类实现了response接口中的所有方法,但这些方法的内部实现都是仅仅调用了一下所包装的response对象的对应方法,以避免用户在对response对象进行增强时需要实现response接口中的所有方法。

response增强案例——压缩响应正文内容

应用HttpServletResponseWrapper对象,压缩响应正文内容。
具体思路:通过filter向目标页面传递一个自定义的response对象。在自定义的response对象中,重写getOutputStream方法和getWriter方法,使目标资源调用此方法输出页面内容时,获得的是我们自定义的ServletOutputStream对象。在我们自定义的ServletOuputStream对象中,重写write方法,使写出的数据写出到一个buffer中。当页面完成输出后,在filter中就可得到页面写出的数据,从而我们可以调用GzipOutputStream对数据进行压缩后再写出给浏览器,以此完成响应正文的压缩功能。
现在我们就来编写这样的一个压缩过滤器,老实说编写这样的一个东西,还真是特别麻烦的,但也不要怕,一步、两步、一步、两步…就能写出来了。
我们要写这样的一个压缩过滤器,可以从后面来推导,按照人的习惯来说,终究是要调用一个Servlet来将数据输出给浏览器的,而这样的Servlet也是大概要有这句代码的:

response.getOutputStream().write(......);

response.getWriter().write(......);

试想别人一调用response的getOutputStream方法和getWriter方法,我就应该返回我自己的流给他们,当别人拿到这样的流对象后,再调用它的方法写数据时,我应控制数据写到一个缓冲里面去。顺理成章的,应使用Decorator设计模式增强response对象,通过我们要编写的压缩过滤器向目标页面传递一个自定义的response对象,在自定义的response对象中,重写getOutputStream方法和getWriter方法。
首先考虑我们在自定义的response对象中重写getOutputStream方法。
怎么来编写自定义的response对象呢?有了上面的想法,我们试着来写代码来实现这样的想法。

class MyResponse extends HttpServletResponseWrapper {    private ByteArrayOutputStream bout = new ByteArrayOutputStream(); // 缓冲流,用字节数组作为缓冲    private HttpServletResponse response;    public MyResponse(HttpServletResponse response) {        super(response);        this.response = response;    }    @Override    public ServletOutputStream getOutputStream() throws IOException {        return new XxxServletOutputStream(bout);    }    // 得到底层流中的数据    public byte[] getBuffer() {        return bout.toByteArray();    }}

大概来说,当别人一调用getOutputStream()方法时,这个方法应返回我自己的ServletOutputStream流对象出去,当别人调用我的ServletOutputStream流对象写数据的时候,我应控制数据写到缓冲流bout里面去。
注意:当别人一调用getOutputStream()方法的话,由这个方法的返回值决定了我必须要返回一个ServletOutputStream对象,我这时就不能返回原来的ServletOutputStream对象了,应该new出一个自己的ServletOutputStream对象返回出去。我要new一个自己的ServletOutputStream对象,那么就应该去查阅Servlet API,可以查到ServletOutputStream是一个抽象类,不可new。既然不可new,那么这时就要写一个子类继承ServletOutputStream,并实现其抽象方法即可。于是,我们自定义的response对象就应该是这样的:

class MyResponse extends HttpServletResponseWrapper {    private ByteArrayOutputStream bout = new ByteArrayOutputStream(); // 缓冲流,用字节数组作为缓冲    private HttpServletResponse response;    public MyResponse(HttpServletResponse response) {        super(response);        this.response = response;    }    @Override    public ServletOutputStream getOutputStream() throws IOException {        return new MyServletOutputStream(bout);    }    // 得到底层流中的数据    public byte[] getBuffer() {        return bout.toByteArray();    }}class MyServletOutputStream extends ServletOutputStream {    private ByteArrayOutputStream bout;    public MyServletOutputStream(ByteArrayOutputStream bout) {        this.bout = bout;    }    @Override    public void write(int b) throws IOException {        bout.write(b);    }    @Override    public boolean isReady() {        // TODO Auto-generated method stub        return false;    }    @Override    public void setWriteListener(WriteListener listener) {        // TODO Auto-generated method stub    }}

写完这样的自定义response对象,还要通过我们写的压缩过滤器传递给目标页面,当页面完成输出后,在压缩过滤器中就可得到页面写出的数据,从而我们可以调用GzipOutputStream对数据进行压缩后再写出给浏览器,以此完成响应正文的压缩功能。

public class GzipFllter implements Filter {    @Override    public void init(FilterConfig filterConfig) throws ServletException {        // TODO Auto-generated method stub    }    @Override    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)            throws IOException, ServletException {        HttpServletRequest request = (HttpServletRequest) req;        HttpServletResponse response = (HttpServletResponse) resp;        MyResponse myResponse = new MyResponse(response);        chain.doFilter(request, myResponse); // 不能直接放行,应增强response的getOutputStream()方法和response的getWriter()方法再放行        // 取出缓冲中的数据压缩后输出        byte[] out = myResponse.getBuffer(); // 得到目标资源的输出        System.out.println("压之前:" + out.length);        byte[] gzipout = gzip(out);        System.out.println("压之后:" + gzipout.length);        response.setHeader("Content-Encoding", "gzip");        response.setHeader("Content-Length", gzipout.length+"");        response.getOutputStream().write(gzipout);    }    // 压缩数据    public byte[] gzip(byte[] b) throws IOException {        ByteArrayOutputStream bout = new ByteArrayOutputStream();        GZIPOutputStream gout = new GZIPOutputStream(bout);        gout.write(b);        gout.close();        return bout.toByteArray();    }    // 内部类    class MyResponse extends HttpServletResponseWrapper {        private ByteArrayOutputStream bout = new ByteArrayOutputStream(); // 缓冲流,用字节数组作为缓冲        private HttpServletResponse response;        public MyResponse(HttpServletResponse response) {            super(response);            this.response = response;        }        @Override        public ServletOutputStream getOutputStream() throws IOException {            return new MyServletOutputStream(bout);        }        // 得到底层流中的数据        public byte[] getBuffer() {            return bout.toByteArray();        }    }    class MyServletOutputStream extends ServletOutputStream {        private ByteArrayOutputStream bout;        public MyServletOutputStream(ByteArrayOutputStream bout) {            this.bout = bout;        }        @Override        public void write(int b) throws IOException {            bout.write(b);        }        @Override        public boolean isReady() {            // TODO Auto-generated method stub            return false;        }        @Override        public void setWriteListener(WriteListener listener) {            // TODO Auto-generated method stub        }    }    @Override    public void destroy() {        // TODO Auto-generated method stub    }}

接着不要忘记在web.xml中配置压缩过滤器哟。

<filter>    <filter-name>CharacterEncodingFilter2</filter-name>    <filter-class>cn.itcast.web.filter.example.CharacterEncodingFilter2</filter-class></filter><filter-mapping>    <filter-name>CharacterEncodingFilter2</filter-name>    <url-pattern>/*</url-pattern></filter-mapping><filter>    <filter-name>GzipFllter</filter-name>    <filter-class>cn.itcast.web.filter.example.GzipFllter</filter-class></filter><filter-mapping>    <filter-name>GzipFllter</filter-name>    <url-pattern>/*</url-pattern></filter-mapping>

不要奇怪以上的配置里面怎么会多了一个CharacterEncodingFilter2,这是一个解决全站中文乱码的过滤器,关于怎么编写这样的一个过滤器可以参见我的笔记Filter高级开发(一)——使用Decorator模式包装request对象解决get和post请求方式下的中文乱码问题哟!
既然编写好了这样的压缩过滤器,那么我们顺便编写一个Servlet来输出数据给浏览器显示。

public class ServletDemo3 extends HttpServlet {    protected void doGet(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        String data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa";        response.getOutputStream().write(data.getBytes());    }    protected void doPost(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        doGet(request, response);    }}

测试,我们通过Firefox浏览器访问该ServletDemo3,浏览器显示正常。你高兴太早了,哈哈!!!千万不要以为这样测试完就万事大吉了,对于咱中国人来说,始终绕不开的一个话题,那就是中文数据乱码的问题了,哎!咱中国人写代码真是好麻烦啊!试想:如果ServletDemo3的代码是这样的:

public class ServletDemo3 extends HttpServlet {    protected void doGet(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        String data = "中国中国中国中国  中国";        response.getOutputStream().write(data.getBytes());    }    protected void doPost(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        doGet(request, response);    }}

再次通过Firefox浏览器访问该ServletDemo3,浏览器显示就会不正常了,出现中文乱码,出现此现象的原因也并不非我们写的压缩过滤器有问题,而是data.getBytes()这句代码有问题,data.getBytes()这句代码把”中国中国中国中国 中国”字符串转成一个字节数组,这时不可避免就要查阅一个码表,可它竟然查的是本地平台的码表——即查的是GBK码表,但response的头在CharacterEncodingFilter2过滤器设置的是UTF-8码表,这时明显写出去的是GBK的数据,而你控制浏览器以UTF-8的方式打开,那肯定就会出现中文乱码的情况了啊!所以我们应将ServletDemo3的代码修改为:

public class ServletDemo3 extends HttpServlet {    protected void doGet(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        String data = "中国中国中国中国  中国";        response.getOutputStream().write(data.getBytes("UTF-8"));    }    protected void doPost(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        doGet(request, response);    }}

至此,我们重写自定义的response对象的getOutputStream()方法就告一段落了。接下来考虑我们在自定义的response对象中重写getWriter方法。
有了想法,我们就来编码实现:

// 内部类class MyResponse extends HttpServletResponseWrapper {    private ByteArrayOutputStream bout = new ByteArrayOutputStream(); // 缓冲流,用字节数组作为缓冲    private HttpServletResponse response;    public MyResponse(HttpServletResponse response) {        super(response);        this.response = response;    }    @Override    public ServletOutputStream getOutputStream() throws IOException {        return new MyServletOutputStream(bout);    }    @Override    public PrintWriter getWriter() throws IOException {        return new PrintWriter(bout);    }    // 得到底层流中的数据    public byte[] getBuffer() {        return bout.toByteArray();    }}

如果像上面那样重写自定义的response对象的getWriter()方法,你觉得可行吗?你说行傻子都不信啊!这样写代码是不行的,由于PrintWriter这个流是一个包装流,如果写出的数据量太小,它是不会写到底层流里面去的,而是写到缓冲里面,你若这样写代码,等会你从底层流bout里面拿数据,就会拿不到数据哟!若要解决,则须在自定义的response对象中维护一个PrintWriter对象,并要修改得到底层流中数据的方法——getBuffer(),那么修改完后的代码即为:

// 内部类class MyResponse extends HttpServletResponseWrapper {    private ByteArrayOutputStream bout = new ByteArrayOutputStream(); // 缓冲流,用字节数组作为缓冲    private PrintWriter pw;    private HttpServletResponse response;    public MyResponse(HttpServletResponse response) {        super(response);        this.response = response;    }    @Override    public ServletOutputStream getOutputStream() throws IOException {        return new MyServletOutputStream(bout);    }    @Override    public PrintWriter getWriter() throws IOException {        pw = new PrintWriter(bout);        return pw;    }    // 得到底层流中的数据    public byte[] getBuffer() {        if (pw != null) {            pw.close();        }        return bout.toByteArray();    }}

同样我们要测试,编写一个Servlet来输出数据给浏览器显示。

public class ServletDemo3 extends HttpServlet {    protected void doGet(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        String data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa";        response.getWriter().write(data);    }    protected void doPost(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        doGet(request, response);    }}

测试,我们通过Firefox浏览器访问该ServletDemo3,浏览器显示正常。输出英文数据总是正常的,而若我们输出中文数据呢?就像下面这样:

public class ServletDemo3 extends HttpServlet {    protected void doGet(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        String data = "中国中国中国中国  中国";        response.getWriter().write(data);    }    protected void doPost(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        doGet(request, response);    }}

终究还是会显示乱码的,而此时出现中文乱码的原因就是我们写的压缩过滤器有问题了,因为response.getWriter()得到的是我new的PrintWriter对象,然后PrintWriter.write("中国中国中国中国 中国");控制这串中文字符写到底层流ByteArrayOutputStream中,要想把该字符串往底层流(即字节数组)里面写,PrintWriter这个类内部在写字符串”中国中国中国中国 中国”出去时,需要查码表,但查的是GBK码表,反正查的不是UTF-8码表。若要解决,则须修改自定义的response对象的getWriter()方法:

public PrintWriter getWriter() throws IOException {    pw = new PrintWriter(new OutputStreamWriter(bout, response.getCharacterEncoding())); // response.getCharacterEncoding()返回的是"UTF-8"    return pw;}

至此,我们重写自定义的response对象的getWriter()方法就告一段落了。
我大概还是怕有人坚持不到这里,也怕有人看的糊里糊涂,遂在此处贴出压缩过滤器的完整代码:

public class GzipFllter implements Filter {    @Override    public void init(FilterConfig filterConfig) throws ServletException {        // TODO Auto-generated method stub    }    @Override    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)            throws IOException, ServletException {        HttpServletRequest request = (HttpServletRequest) req;        HttpServletResponse response = (HttpServletResponse) resp;        MyResponse myResponse = new MyResponse(response);        chain.doFilter(request, myResponse); // 不能直接放行,应增强response的getOutputStream()方法和response的getWriter()方法再放行        // 取出缓冲中的数据压缩后输出        byte[] out = myResponse.getBuffer(); // 得到目标资源的输出        System.out.println("压之前:" + out.length);        byte[] gzipout = gzip(out);        System.out.println("压之后:" + gzipout.length);        response.setHeader("Content-Encoding", "gzip");        response.setHeader("Content-Length", gzipout.length+"");        response.getOutputStream().write(gzipout);    }    // 压缩数据    public byte[] gzip(byte[] b) throws IOException {        ByteArrayOutputStream bout = new ByteArrayOutputStream();        GZIPOutputStream gout = new GZIPOutputStream(bout);        gout.write(b);        gout.close();        return bout.toByteArray();    }    // 内部类    class MyResponse extends HttpServletResponseWrapper {        private ByteArrayOutputStream bout = new ByteArrayOutputStream(); // 缓冲流,用字节数组作为缓冲        private PrintWriter pw;        private HttpServletResponse response;        public MyResponse(HttpServletResponse response) {            super(response);            this.response = response;        }        @Override        public ServletOutputStream getOutputStream() throws IOException {            return new MyServletOutputStream(bout);        }        @Override        public PrintWriter getWriter() throws IOException {            pw = new PrintWriter(new OutputStreamWriter(bout, response.getCharacterEncoding())); // response.getCharacterEncoding()返回的是"UTF-8"            return pw;        }        // 得到底层流中的数据        public byte[] getBuffer() {            if (pw != null) {                pw.close();            }            return bout.toByteArray();        }    }    class MyServletOutputStream extends ServletOutputStream {        private ByteArrayOutputStream bout;        public MyServletOutputStream(ByteArrayOutputStream bout) {            this.bout = bout;        }        @Override        public void write(int b) throws IOException {            bout.write(b);        }        @Override        public boolean isReady() {            // TODO Auto-generated method stub            return false;        }        @Override        public void setWriteListener(WriteListener listener) {            // TODO Auto-generated method stub        }    }    @Override    public void destroy() {        // TODO Auto-generated method stub    }}

虽然我们的压缩过滤器是写好了,但是在web.xml文件中配置该过滤器时,我们拦截的是所有资源,即有人访问服务器中的xxx.jsp、xxx.jpg(png/gif)等时,都会将数据压缩之后再发送给浏览器,我们还要考虑若是有人下载一部电影,电影的容量大概在1G左右,这么大的数据量,要用一个字节数组去存储,势必内存会爆炸,所以我们不应该拦截所有资源,而是有选取的拦截一些资源,又因为gzip这种压缩格式只对文本格式数据的压缩效率最高,所以我们可以在web.xml文件中这样配置压缩过滤器:

<filter>    <filter-name>GzipFllter</filter-name>    <filter-class>cn.itcast.web.filter.example.GzipFllter</filter-class></filter><filter-mapping>    <filter-name>GzipFllter</filter-name>    <url-pattern>*.jsp</url-pattern>    <dispatcher>FORWARD</dispatcher>    <dispatcher>REQUEST</dispatcher></filter-mapping><filter-mapping>    <filter-name>GzipFllter</filter-name>    <url-pattern>*.html</url-pattern></filter-mapping><filter-mapping>    <filter-name>GzipFllter</filter-name>    <url-pattern>*.js</url-pattern></filter-mapping><filter-mapping>    <filter-name>GzipFllter</filter-name>    <url-pattern>*.css</url-pattern></filter-mapping>

我们应该尤其注意该过滤器所拦截的jsp资源被Servlet容器调用的方式,不管是用户直接访问页面,还是目标资源是通过RequestDispatcher的forward()方法访问,该过滤器都应予以拦截。

0 0