Java Servlet GZIP commpression filter 

Joined:
04/09/2007
Posts:
773

November 23, 2010 20:20:01    Last update: March 01, 2011 13:38:51
I tried to find a GZIP compression servlet filter to compress a large log file that we send down to the browser. Most of the implementations I found were overly complicated and many buggy. This is a simple implementation that worked for me.

The filter:
package filter.demo;

import java.io.*;
import java.util.*;
import java.util.zip.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class GZIPServletFilter implements Filter {
    private static final Log log = LogFactory.getLog(GZIPServletFilter.class);

    public void init(FilterConfig cfg) throws ServletException {
    }

    public void destroy() {
    }

    public void doFilter(ServletRequest req,
		         ServletResponse resp,
			 FilterChain chain) 
		throws IOException, ServletException {
	log.debug("Gzip filter");

	boolean supportsGzip = false;
        Enumeration e = ((HttpServletRequest)req).getHeaders("Accept-Encoding");
        while (e.hasMoreElements()) {
            String name = (String) e.nextElement();
            if (name.matches("(?i).*gzip.*")) {
		supportsGzip = true;
		break;
            }
	}

	if (supportsGzip) {
	    log.debug("Gzip supported");
	    HttpServletResponse httpResp = (HttpServletResponse) resp;
	    // This does NOT work for Tomcat! 
	    // Tomcat JSP does not close the output stream upon finishing, so we have to close it explicitly.
	    // chain.doFilter(req, new GZIPServletResponseWrapper(httpResp));
	    HttpServletResponseWrapper wrapper = new GZIPServletResponseWrapper(httpResp);
	    chain.doFilter(req, wrapper);
	    wrapper.getOutputStream().close();
	}
	else {
	    log.debug("Gzip NOT supported!");
	    chain.doFilter(req, resp);
	}
    }

    class GZIPServletResponseWrapper extends HttpServletResponseWrapper {
	private GZIPOutputStream gzipStream;
	private ServletOutputStream servletOutputStream;
	private PrintWriter printWriter;

	GZIPServletResponseWrapper(HttpServletResponse resp) throws IOException {
	    super(resp);
	}
	
	public ServletOutputStream getOutputStream() throws IOException {
	    if (servletOutputStream == null) {
		servletOutputStream = createOutputStream();
	    }
	    return servletOutputStream;
	}

	public PrintWriter getWriter() throws IOException {
	    if (printWriter == null) {
		printWriter = new PrintWriter(new OutputStreamWriter(
				getOutputStream(),
				getCharacterEncoding())) {  // This is important for I18N
                    // Workaround for Tomcat bug where flush is NOT called when JSP output finished
		    public void write(char[] cb, int off, int len) {
			super.write(cb, off, len);
			super.flush();
		    }
		};
	    }
	    return printWriter;
	}

	private ServletOutputStream createOutputStream() throws IOException {
	    ServletResponse resp = this.getResponse();
	    gzipStream = new GZIPOutputStream(resp.getOutputStream());
	    addHeader("Content-Encoding", "gzip");
	    addHeader("Vary", "Accept-Encoding");
	    return new ServletOutputStream() {
		/* The first three methods must be overwritten */
		@Override
		public void write(int b) throws IOException {
		    gzipStream.write(b);		
		}

		@Override
		public void flush() throws IOException {
		    gzipStream.flush();
		}

		@Override
		public void close() throws IOException {
		    gzipStream.close();
		}

		/*
		 * These two are not absolutely needed. They are here simply
		 * because they were overriden by GZIPOutputStream.
		 */
		@Override
		public void write(byte[] b) throws IOException {
		    gzipStream.write(b);
		}

		@Override
		public void write(byte[] b, int off, int len) throws IOException {
		    gzipStream.write(b, off, len);
		}
	    };
	}
    }
}


Config web.xml:
<filter>
    <filter-name>gzipFilter</filter-name>
    <filter-class>filter.demo.GZIPServletFilter</filter-class>
</filter>

<!-- add filter for servlet. URL mapping may also be used. -->
<filter-mapping>
    <filter-name>gzipFilter</filter-name>
    <servlet-name>theMainServlet</servlet-name>
</filter-mapping>


The ugly anonymous inner class could have been avoided if the servlet API did not insist on ServletResponse.getOutputStream returning the bogus ServletOutputStream class (instead of the plain OutputStream).

Additional Note: In an earlier version of this filter, the gzip headers were added in doFilter, like this:
// This is NOT good!
if (supportsGzip) {
    log.debug("Gzip supported");
    HttpServletResponse httpResp = (HttpServletResponse) resp;
    httpResp.addHeader("Content-Encoding", "gzip");
    httpResp.addHeader("Vary", "Accept-Encoding");
    chain.doFilter(req, new GZIPServletResponseWrapper(httpResp));
}

It turned out that the ServletResponse methods sendError bypasses the gzip response wrapper, and you end up with a response with the gzip header set but the content is not compressed. Therefore, the addHeader calls are moved to createServletOutputStream.
Share |
| Comment  | Tags