25

Following Google's pagespeed advice I would like the minify the HTML responses of my Spring application. I don't mean GZip, I mean removing comments and whitespace from HTML before it is sent down the wire.

I would like to do this dynamically and not in my templates. My templates contain many comments that are useful but should not be part of the response.

Following is my controller;

@Controller
public class IndexController {

    @GetMapping("/")
    public ModelAndView index() {
        Data data = ....
        return new ModelAndView("index", data);
    }
}
buræquete
  • 14,226
  • 4
  • 44
  • 89
JackMahoney
  • 3,423
  • 7
  • 32
  • 50
  • 3
    For those who think minifying HTML is a bad idea, see this google link for why I want to do this: https://developers.google.com/speed/docs/insights/MinifyResources – JackMahoney Sep 26 '17 at 18:15

1 Answers1

22

I managed to do this by adding a javax.servlet.Filter component that is using com.googlecode.htmlcompressor into Spring

First the Filter;

@Component
public class HtmlFilter implements Filter {
    protected FilterConfig config;

    public void init(FilterConfig config) throws ServletException {
        this.config = config;
    }

    public void destroy() {
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        ServletResponse newResponse = response;

        if (request instanceof HttpServletRequest) {
            newResponse = new CharResponseWrapper((HttpServletResponse) response);
        }

        chain.doFilter(request, newResponse);

        if (newResponse instanceof CharResponseWrapper) {
            String text = newResponse.toString();
            if (text != null) {
                HtmlCompressor htmlCompressor = new HtmlCompressor();
                response.getWriter().write(htmlCompressor.compress(text));
            }
        }
    }
}

and relevant CharResponseWrapper;

class CharResponseWrapper extends HttpServletResponseWrapper {
    protected CharArrayWriter charWriter;
    protected PrintWriter writer;
    protected boolean getOutputStreamCalled;
    protected boolean getWriterCalled;

    public CharResponseWrapper(HttpServletResponse response) {
        super(response);

        charWriter = new CharArrayWriter();
    }

    public ServletOutputStream getOutputStream() throws IOException {
        if (getWriterCalled) {
            throw new IllegalStateException("getWriter already called");
        }

        getOutputStreamCalled = true;
        return super.getOutputStream();
    }

    public PrintWriter getWriter() throws IOException {
        if (writer != null) {
            return writer;
        }
        if (getOutputStreamCalled) {
            throw new IllegalStateException("getOutputStream already called");
        }
        getWriterCalled = true;
        writer = new PrintWriter(charWriter);
        return writer;
    }

    public String toString() {
        String s = null;

        if (writer != null) {
            s = charWriter.toString();
        }
        return s;
    }
}

Works fantastically. Converts an html this ugly;

<!DOCTYPE HTML>
<html>
<head>
    <title>
        A Simple
        <!--        Test-->
        HTML Document
        <!--        Test-->

    </title>



</head>
<body>
                 <p>This is a very simple HTML document</p>


                 <!--        Test-->



<p>It only has two<!--        Test--> paragraphs</p>

                 <!--        Test-->

</body>
</html>

into this;

<!DOCTYPE HTML> <html> <head> <title> A Simple HTML Document </title> </head> <body> <p>This is a very simple HTML document</p> <p>It only has two paragraphs</p> </body> </html>
buræquete
  • 14,226
  • 4
  • 44
  • 89
  • If you don't use statistics, htmlCompressor may be cached in a class member – cdalxndr Sep 20 '19 at 11:48
  • 1
    Am i wrong or is the method `getOutputStream()` in class `CharResponseWrapper` not necessary and can be deleted safely? Seems to work anyway... – manu Oct 31 '20 at 11:32