Created
February 6, 2019 11:01
-
-
Save nealeu/7956485d52411b2e6f1b739d0b61078e to your computer and use it in GitHub Desktop.
Spring Framework caching of Etagged responses
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); | |
} | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.