17

I am looking for a GZIP servlet filter to be used in a high volume web-app. I doesn't want to use the container specific options.

Requirement

  1. Ability to compress response payload (XML)
  2. Faster
  3. Proven in production for high volume applications
  4. Should properly set appropriate Content-Encoding
  5. portable across containers
  6. Optionally able to decompress request

Thank you.

Aravind Yarram
  • 78,777
  • 46
  • 231
  • 327
  • *And content length*? That's going to be memory hogging. Just let it to be sent in chunks, like everyone does. – BalusC Jan 24 '11 at 12:09
  • @BalusC Thanks for clarifying this. You are correct. I will remove this from the post. – Aravind Yarram Jan 24 '11 at 14:29
  • I haven't tried it, but this looks very promising for compression of CSS and JS: http://wro4j.readthedocs.io/en/stable/. – mm759 May 31 '17 at 20:56

5 Answers5

21

The GZIP filter that I use to compress resources in my webapps:

public class CompressionFilter implements Filter {

    public void destroy() {
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String acceptEncoding = httpRequest.getHeader(HttpHeaders.ACCEPT_ENCODING);
        if (acceptEncoding != null) {
            if (acceptEncoding.indexOf("gzip") >= 0) {
                GZIPHttpServletResponseWrapper gzipResponse = new GZIPHttpServletResponseWrapper(httpResponse);
                chain.doFilter(request, gzipResponse);
                gzipResponse.finish();
                return;
            }
        }
        chain.doFilter(request, response);
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }

}

public class GZIPHttpServletResponseWrapper extends HttpServletResponseWrapper {

    private ServletResponseGZIPOutputStream gzipStream;
    private ServletOutputStream outputStream;
    private PrintWriter printWriter;

    public GZIPHttpServletResponseWrapper(HttpServletResponse response) throws IOException {
        super(response);
        response.addHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
    }

    public void finish() throws IOException {
        if (printWriter != null) {
            printWriter.close();
        }
        if (outputStream != null) {
            outputStream.close();
        }
        if (gzipStream != null) {
            gzipStream.close();
        }
    }

    @Override
    public void flushBuffer() throws IOException {
        if (printWriter != null) {
            printWriter.flush();
        }
        if (outputStream != null) {
            outputStream.flush();
        }
        super.flushBuffer();
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (printWriter != null) {
            throw new IllegalStateException("printWriter already defined");
        }
        if (outputStream == null) {
            initGzip();
            outputStream = gzipStream;
        }
        return outputStream;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (outputStream != null) {
            throw new IllegalStateException("printWriter already defined");
        }
        if (printWriter == null) {
            initGzip();
            printWriter = new PrintWriter(new OutputStreamWriter(gzipStream, getResponse().getCharacterEncoding()));
        }
        return printWriter;
    }

    @Override
    public void setContentLength(int len) {
    }

    private void initGzip() throws IOException {
        gzipStream = new ServletResponseGZIPOutputStream(getResponse().getOutputStream());
    }

}

public class ServletResponseGZIPOutputStream extends ServletOutputStream {

    GZIPOutputStream gzipStream;
    final AtomicBoolean open = new AtomicBoolean(true);
    OutputStream output;

    public ServletResponseGZIPOutputStream(OutputStream output) throws IOException {
        this.output = output;
        gzipStream = new GZIPOutputStream(output);
    }

    @Override
    public void close() throws IOException {
        if (open.compareAndSet(true, false)) {
            gzipStream.close();
        }
    }

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

    @Override
    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        if (!open.get()) {
            throw new IOException("Stream closed!");
        }
        gzipStream.write(b, off, len);
    }

    @Override
    public void write(int b) throws IOException {
        if (!open.get()) {
            throw new IOException("Stream closed!");
        }
        gzipStream.write(b);
    }

}

You also need to define the mapping in your web.xml:

<filter>
    <filter-name>CompressionFilter</filter-name>
    <filter-class>com.my.company.CompressionFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CompressionFilter</filter-name>
    <url-pattern>*.js</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>CompressionFilter</filter-name>
    <url-pattern>*.css</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>CompressionFilter</filter-name>
    <url-pattern>*.html</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>CompressionFilter</filter-name>
    <url-pattern>*.jsp</url-pattern>
</filter-mapping>
bookL
  • 17
  • 5
German Attanasio
  • 22,217
  • 7
  • 47
  • 63
  • The formatting of this code is not perfect, but it actually works for me, without any dependency. Thanks a bunch! – electrotype Dec 24 '12 at 16:34
  • Thank you for providing full implementations for all the classes, I prefer this small solution to importing a whole new dependency (which comes with a bunch of dependencies of its own). Question, why do map to js/css/html/jsp specifically, what types of streams would you not want to compress? – SergeyB Oct 14 '13 at 19:15
  • 2
    Images, executable files or any other file type that will not take advantages of compression. – German Attanasio Oct 15 '13 at 03:18
  • Please not that this could break `If-Match` support, unless `ETag` doesn't get managed (usually with a `-gzip` suffix). – lapo Jan 24 '17 at 11:13
  • Would be much appreciated if there were implementations of `isReady()` and `setWriteListener(WriteListener writeListener)` in ServletResponseGZIPOutputStream. These methods came with Servlet 3.1. – Dmytro Moskovchenko Mar 30 '21 at 15:11
17

From what i've seen, most people generally use the gzip compression filter. Typically from ehcache.

The GZIP filter implementation is: net.sf.ehcache.constructs.web.filter.GzipFilter

The Maven coordinate for including it in your project is:

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-web</artifactId>
    <version>2.0.4</version>
</dependency>

You will also need to specify an SLF4J logging target. If you don't know what this is or don't care slf4j-jdk14 or slf4j-simple works:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>1.6.4</version>
</dependency>
mckamey
  • 17,359
  • 16
  • 83
  • 116
Steven
  • 3,844
  • 3
  • 32
  • 53
  • any specific reason why this is preferred? – Aravind Yarram Jan 21 '11 at 14:15
  • could just be word of mouth. or that a lot of projects already use ehcache. either way, it has always seemed reliable and quick. – Steven Jan 21 '11 at 23:14
  • This worked great for me. I just added links to the mentioned implementation for anyone else trying this out who has not heard of ehcache. – mckamey Nov 18 '11 at 17:57
  • How is it possible to apply GzipFilter only to some specified mime typeS? (SO: http://stackoverflow.com/questions/18061482/apply-ehcache-gzipfilter-to-specific-mime-types) – Xdg Aug 06 '13 at 06:02
  • @Xdg You might need to extend the GzipFilter to do that. – Steven Aug 06 '13 at 10:03
  • 1
    If you only use the `GzipFilter` from `ehcache-web` and want to avoid bloating your app, you can exclude `ehcache-web`'s transitive dependency `ehcache-core` in your `pom.xml` like so `net.sf.ehcache>ehcache-core` – schnatterer Jul 20 '16 at 09:52
  • net.sf.ehcache.constructs.web.filter.GzipFilter in method doFilter() - seems like this filter stores full request/response data in a memory - which can cause memory problems with large requests (or big file uploads). – StrekoZ Jul 26 '17 at 07:44
2

Check out the pjl-comp-filter CompressingFilter:

http://sourceforge.net/projects/pjl-comp-filter/

jamesmortensen
  • 33,636
  • 11
  • 99
  • 120
2

I would recommend you use something in-front of tomcat to off-load gzipping. Apache with mod_deflate will perform well. You have the option of putting apache on the same box, or moving it off to a different box, that way compression doesn't impact your app at all. mod_jk or mod_proxy will both work fine in this setup.

http://httpd.apache.org/docs/2.0/mod/mod_deflate.html

Zeki
  • 5,107
  • 1
  • 20
  • 27
  • I want this solution to be portable so this is not the solution I am looking for. – Aravind Yarram Jan 21 '11 at 04:33
  • How do you know he's using Tomcat? I use Jetty, which is a full web server, and they don't recommend putting Apache in front of it. – jamesmortensen Jan 21 '11 at 06:04
  • @jmort253 - there are several other reasons you might want to use apache in front of jetty. e.g. might be for load balancing several jetty instances. – Steven Jan 21 '11 at 07:07
0

Or if you're using Nginx in front see here: http://nginx.org/en/docs/http/ngx_http_gzip_module.html. But definitely, as Zeki said it is better to move this to the dedicated web server.

Adrian Ber
  • 20,474
  • 12
  • 67
  • 117