Minify all your JavaScript and Stylsheets automatically with YUI Compressor and a Servlet Filter

9

In this article I will explain how to minify all your .js and .css files automatically with a servlet filter. This means you only have to define a servlet filter in your web.xml and everything works!

In the project I’m currently working on we have some pretty large JavaScripts and CSS files. Those files also contain comments that the users don’t need to use the application. So I wanted to be able to remove unnecessary whitespace, line breaks and comments. When I read an article on Ajaxian about the YUI Compressor library I got curious. I assumed the library was written in C or another language that doesn’t start with J and ends with ava. But I got lucky, it was a Java library!

....

I hate to do thing manually or create setup files, so I came up with a solution: a Servlet Filter for .css and .js files. That idea sounds so simple that there has to be someone who already did that. I searched for a while and the only thing I found was someone who said he was going to make it and it seems he wants to see some money for it! Well that’s not so cool. YUI Compressor is free and when you make a simple thing like a servlet filter you should donate it to the community. I’m sure someone already created a servlet filter, but that person should’ve posted the article on DZone or javablogs so other people know it exists.

But enough talking let me show you what I did. I won’t describe every little detail, I only want to give you an idea what I did. I will provide the source code so you can see what’s done exactly and if you have any questions about that I will answer them (in the comments of this article).

Servlet Filter

The first step is setting up the servlet filter and find out what the input and output of the filter is. A Servlet Filter is nothing more than a class that implements the javax.servlet.Filter interface.
A servlet filter consists of three methods:

public void init(FilterConfig filterConfig)<br />public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)<br />public void destroy()<br />

The init method is the place where you can read input parameters and setup the filter. This method acts as a setter for a private filterConfig field and reads the filter init parameters. The filter init parameters are the same parameters you can pass to the YUI Compressor library. I choose not to use all the parameters, just a subset with the most important ones.

The doFilter() method checks whether the compressed file already is in our cache and writes it to the ServletOutputStream. The input for the YUI Compressor is a java.io.Reader object. We can get a Reader object with the following code:

String requestURI = request.getRequestURI();<br />InputStream inputStream = context.getResourceAsStream(requestURI);<br />InputStreamReader isr = new InputStreamReader(inputStream);

The isr object can be passed to the compressor. The compressor writes the output to the cache (with a StringWriter object). When a file is found in the cache it’s written to the ServletOutputStream:

ServletOutputStream servletOutputStream = response.getOutputStream();

The servletOutputStream is a parameter of the Compressor.

The destroy() method is empty, because we have no sepcial things that need to be cleaned up (like open files or connections to external resources).

YUI Compressor

The second step was understanding the YUI Compressor.  I used the 2.1.2 version, but everything I’m about to tell should work with older and newer versions. In the download on the page the source code is included. The YUI Compressor class is the class we will use as a reference for the Filter, it contains all the important information we need.
The primary goal is compressing some input. The input the YUIC needs is a java.io.Reader object and an ErrorReporter object.

JavaScriptCompressor compressor = new JavaScriptCompressor(in, new ErrorReporter() { ... });<br />

The ErrorReporter object is something I created myself. My class is called CompressorFilterErrorReporter and it implements the ErrorReporter interface. I will skip this class, it’s quite straightforward what needs to be done and you can look it up in the source code. You only need to know that it prints some error messages.
The last step of the compressor is invoking the compress method. You have to pass the command line parameters and the Write you want to write to. Because we make use of a cache we write to a String with a StringWriter.

The CSS compressor works basically the same as the JavaScriptCompressor, but needs fewer parameters.

Caching

It turned out the compressor was pretty slow (on my laptop it almost took 1 second to compress the jQuery library). Of course it’s a bit useless to compress a script everytime a request is made so had to introduce caching anyway. I use a Hashtable (yes, with a lower case t)  to cache the scripts, the cache won’t expire, there’s no round robin etc. You usally won’t have hundreds of scripts and stylesheets in the cache so a normal Hashtable will be enough.
Instead of writing directly to the ServletOutputStream I now write to a String and put this String with the Request URI in the Hashtable. It’s a simple caching mechanism, but it works pretty good.
The first time the scripts are compressed it might take a little longer to load the page but every new request is pretty fast (maybe even faster than a normal request because this one doesn’t need to be read from disk)

Configuration
You have to put the filter in your web.xml. The simplest configuration looks like this:

&lt;filter&gt;<br />&nbsp;&nbsp;&nbsp; &lt;display-name&gt;Yahoo Compressor Filter&lt;/display-name&gt;<br />&nbsp;&nbsp;&nbsp; &lt;filter-name&gt;CompressorFilter&lt;/filter-name&gt;<br />&nbsp;&nbsp;&nbsp; &lt;filter-class&gt;nl.amis.filter.CompressorFilter&lt;/filter-class&gt;<br />&lt;/filter&gt;<br /><br />&lt;filter-mapping&gt;<br />&nbsp;&nbsp;&nbsp; &lt;filter-name&gt;CompressorFilter&lt;/filter-name&gt;<br />&nbsp;&nbsp;&nbsp; &lt;url-pattern&gt;*.js&lt;/url-pattern&gt;<br />&nbsp;&nbsp;&nbsp; &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;<br />&lt;/filter-mapping&gt;<br /><br />&lt;filter-mapping&gt;<br />&nbsp;&nbsp;&nbsp; &lt;filter-name&gt;CompressorFilter&lt;/filter-name&gt;<br />&nbsp;&nbsp;&nbsp; &lt;url-pattern&gt;*.css&lt;/url-pattern&gt;<br />&nbsp;&nbsp;&nbsp; &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;<br />&lt;/filter-mapping&gt;

I decided to work with the default values of YUI Compressor. You only need parameters when you don’t want to use the defaults. When you want to use the parameters you have to put them between </filter-class> and </filter>

&nbsp;&nbsp;&nbsp; &lt;init-param&gt;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;param-name&gt;preserve-semi&lt;/param-name&gt;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;param-value&gt;true&lt;/param-value&gt;<br />&nbsp;&nbsp;&nbsp; &lt;/init-param&gt;

Is used to preserve all the semi colons. Other parameters you can use are line-break, warn and nomunge. Consult the YUI Compressor to see what these parameters do.

Conclusion and download

Click here to download the files. You have to download the YUI Compressor yourself. I also make use of Log4J. When you don’t want to use Log4j you just have to replace it with your own logging library or System.out. There are only 5 logging statements, so it shouldn’t be too much work.
Many thanks to the guy(s) tha
t created the YUI Compressor, i
t’s easy to use and does a great job.
You’re free to use the source code in your application, just don’t remove my name from the Javadoc ;-)
I’d really appreciate some feedback on this piece of code, it probably can be improved.

Sources

http://www.julienlecomte.net/blog/2007/08/13/introducing-the-yui-compressor/
http://www.julienlecomte.net/yuicompressor/

Share.

About Author

9 Comments

  1. Jordi Hernandez on

    Hi! I have published a java library that does all this and more for you, called Jawr. It is a solution that adds zero processing overhead to requests and supports gzipping and combination of js (and CSS) files. Compression is done by means of JSMin.
    You can check it out at https://jawr.dev.java.net/

  2. thanks for excellent tip.
    However I got some trouble.

    InputStream inputStream = context.getResourceAsStream(requestURI);
    ==> here, inoutSteam is null.
    FYI, requetedURI is “/static/global/css/ng-global.css”.
    And I’m using spring MVC framework.
    What is wrong?

  3. Thanks for this information! I had to make some changes in order to add support for chaining. This way it is possible to add other filters after this one. This example works great as long as this filter is the last on in the list. :)

    Anyway, if you would like to see my example let me know and I will send it to you.

  4. Jeroen van Wilgenburg on

    Good remark Mathias! It felt kind of wrong using something with a lower case t, so now I have a working (and better looking ;) ) solution.

  5. Jeroen van Wilgenburg on

    Thanks for your tips Patrick. The header will improve performance, I’m going to find out how to do it in Tomcat. I think I can change the headers in the servlet filter. I’m not sure whether GZip will improve client side performance, but I also will give that a try. I’m affraid unzipping takes more time than transferring the extra bytes.

  6. I’m not sure why you would need to run though content-specific compression over general gzip (agreeing with Patrick). It seems like code-specific compression would be an extra variable and added overhead when Gzip is content agnostic and is supported by all browsers.

  7. Use a ConcurrentHashMap (JDK 1.5+) instead of the older Hashtable – should improve concurrency performance a lot.

  8. Jeroen,

    if you add the Apache module mod_gzip to your web-server config, you can even shrink it more. Have a look at http://dean.edwards.name/weblog/2007/08/js-compression/
    John Scott has written a white paper about configuring mod_gzip at http://jes.blogs.shellprompt.net/2007/05/18/apex-delivering-pages-in-3-seconds-or-less/ (search for mod_gzip in the white paper).
    Another performance boost is to check that your server sends in the header of the response that the file is cacheable, otherwise your browser will request again it with each page or new session.

    Patrick