Java Servlet GZIP commpression filter
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:
Config
The ugly anonymous inner class could have been avoided if the servlet API did not insist on
Additional Note: In an earlier version of this filter, the
It turned out that the
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.