Skip to content

Instantly share code, notes, and snippets.

@nealeu
Created February 6, 2019 11:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nealeu/7956485d52411b2e6f1b739d0b61078e to your computer and use it in GitHub Desktop.
Save nealeu/7956485d52411b2e6f1b739d0b61078e to your computer and use it in GitHub Desktop.
Spring Framework caching of Etagged responses
@GetMapping("/expensiveEndpoint")
public void findGlobalConfig(WebRequest request, HttpServletResponse response) throws IOException {
// Optionally String myCacheKey = buildKey(params)
etaggedResponseCacheManager.getFromCacheWithEtagHandling(
request, response,
"myCacheName", "myCacheKey", 3600,
() -> {
return expensiveToBuildAndSerialize();
}
);
}
package webApi.support;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.WebRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
@RequiredArgsConstructor
public class EtaggedResponseCacheManager {
private final ObjectMapper objectMapper;
private final CacheManager cacheManager;
public void getFromCacheWithEtagHandling(WebRequest request, HttpServletResponse response,
String cacheName, String key,
int httpCacheSeconds,
Supplier<Object> viewModelSupplier) throws IOException {
Cache cache = cacheManager.getCache(cacheName);
Callable<EtaggedString> cachedValueSupplier = () -> {
String json = objectMapper.writeValueAsString(viewModelSupplier.get());
return new EtaggedString(json);
};
EtaggedString etaggedResult = cache.get(key, cachedValueSupplier);
if (request.checkNotModified(etaggedResult.getEtag())) {
return;
}
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); // Required to ensure IE11 works
cacheForXSecs(httpCacheSeconds, response);
response.getWriter().write(etaggedResult.getPayload()); // Note: flushes headers, so Spring can't add more (I think)
}
static public void cacheForXSecs(int numSeconds, HttpServletResponse response) {
response.addHeader("Cache-Control", "max-age=" + numSeconds);
}
}
package webApi.support;
import lombok.Getter;
import org.springframework.util.DigestUtils;
@Getter
public class EtaggedString {
private final String payload;
/** Shallow Etag of payload */
private final String etag;
public EtaggedString(String payload) {
this.payload = payload;
etag = calculateShallowEtag(payload);
}
private String calculateShallowEtag(String payload) {
return generateETagHeaderValue(payload.getBytes());
}
/** Derived from {@link org.springframework.web.filter.ShallowEtagHeaderFilter#generateETagHeaderValue} */
protected String generateETagHeaderValue(byte[] bytes) {
// length of W/ + 0 + " + 32bits md5 hash + "
StringBuilder builder = new StringBuilder(37);
builder.append("W/\"0");
DigestUtils.appendMd5DigestAsHex(bytes, builder);
builder.append('"');
return builder.toString();
}
}
@nealeu
Copy link
Author

nealeu commented Feb 6, 2019

This is an approach I've taken to caching of a large piece of reference data built from multiple data sources.

We could have cached the call to expensiveToBuildAndSerialize(), but this would still have required serilisation and Etag generation, so instead this stores the serialised result and the Etag.

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