Skip to content

Instantly share code, notes, and snippets.

@mikeapr4
Last active November 30, 2015 19:09
Show Gist options
  • Save mikeapr4/4f96b6807585ad34813c to your computer and use it in GitHub Desktop.
Save mikeapr4/4f96b6807585ad34813c to your computer and use it in GitHub Desktop.
Java Spring Filter for Caching response headers on static and non-static resources (see comment for details)
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.LinkedHashMap;
/**
* Adds Caching response headers based on a request URL mapping.
* Each mapping is configured as FORCE_CHECK or NO_CACHE
*/
public class CacheHeaderFilter extends GenericFilterBean {
private LinkedHashMap<String, CacheMode> cacheMap = new LinkedHashMap<>(); // sorted for pattern priority
private AntPathMatcher matcher = new AntPathMatcher();
public CacheHeaderFilter(LinkedHashMap<String, CacheMode> cacheMap) {
this.cacheMap = cacheMap;
}
// Don't know what headers do what?
// https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers
public static void configCacheHeaders(CacheMode mode, HttpServletResponse res) {
switch (mode) {
case FORCE_CHECK:
res.setHeader("Cache-Control", "private, max-age=0");
break;
case NO_CACHE:
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache");
res.setDateHeader("Expires", 0);
break;
}
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
String servletPath = ((HttpServletRequest) servletRequest).getServletPath();
HttpServletResponse res = (HttpServletResponse) servletResponse;
for (String path : cacheMap.keySet()) {
if (matcher.match(path, servletPath)) {
configCacheHeaders(cacheMap.get(path), res);
break;
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
public enum CacheMode {NO_CACHE, FORCE_CHECK}
}
@mikeapr4
Copy link
Author

Objective: Ensure that new content isn't ignored by the browser's in-built cache, but also to allow sensible caching to occur to avoid the application running sluggish.

When content is sent back to the browser, there are 2 possible instructions to send:

  • Always Check for Stale Content - this is where the browser should cache content that won't change between releases/deployments, but also that shouldn't be blindly cached when in fact it might have been modified.
  • Never Cache - where the same request moments later could produce a different result, this is NOT a static resource and therefore shouldn't follow static resource rules.

Content Classification

Static content includes:

  • Images
  • Stylesheets
  • Javascript scripts
  • HTML Templates
  • Static HTML files

Non-Static content includes:

  • Web Services and Controller responses.

Content Request-Response Flow

Further Reading

https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers

Static Content

First time, the browser requests the content (in this case the root page is a static index.html file)

GET / HTTP/1.1
Host: localhost:8080

HTTP/1.1 200 OK
Cache-Control: private, max-age=0
Content-Type: text/html
Last-Modified: Tue, 21 Apr 2015 16:03:16 GMT
Content-Length: 944

Notice the response contains the last modified date of the content, this corresponds to the file modified date of the static resource, which suits. With the browser told it can cache (private), but with no allowance for stale content (max-age=0), the following request contains a predicate:

GET / HTTP/1.1
Host: localhost:8080
If-Modified-Since: Tue, 21 Apr 2015 16:03:16 GMT

HTTP/1.1 304 Not Modified

An alternative scenario is that the content has legitimately changed, meaning this happens:

GET / HTTP/1.1
Host: localhost:8080
If-Modified-Since: Tue, 21 Apr 2015 16:03:16 GMT

HTTP/1.1 200 OK
Cache-Control: private, max-age=0
Content-Type: text/html
Last-Modified: Tue, 21 Apr 2015 18:14:34 GMT
Content-Length: 956

Non-Static Content

First and all subsequent request/responses will look like this:

GET /api HTTP/1.1
Host: localhost:8080

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json;charset=UTF-8

Sample Code Usage

@Bean
public GenericFilterBean cacheHeaderFilter() {
    final LinkedHashMap<String, CacheMode> cacheMap = new LinkedHashMap<>();
    cacheMap.put("/", CacheHeaderFilter.CacheMode.FORCE_CHECK); // Static
    cacheMap.put("/**/*.*", CacheHeaderFilter.CacheMode.FORCE_CHECK); // Static resources
    cacheMap.put("/**", CacheHeaderFilter.CacheMode.NO_CACHE); // RESTful API call

    return new CacheHeaderFilter(cacheMap);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment