“All programming is an exercise in caching.”
Terje Mathisen
Spring Boot endpoints can be formulated briefly and concisely. Their return values are automatically converted into suitable responses. In this example, the list of users is converted into an HTTP response with a JSON body.
@GetMapping public List<User> getUsers() { return userService.getAllUsers(); }
Occasionally, the developer has to fall back on the ResponseEntity
class as a return value. This is the case, for example, when additional HTTP headers are required, such as the Cache-Control
header.
@GetMapping public ResponseEntity<List<User>> getUsers() { return ResponseEntity.ok().cacheControl(CacheControl.maxAge(Duration.ofSeconds(60))) .body(userService.getAllUsers()); }
The example provides a maxAge
value of 60 seconds as HTTP headers. This means that the corresponding response is cached by a browser for one minute.
The solution works but is considerably more complex compared to the first version. As in many other places, a suitable annotation would be nicer.
The desired implementation is a @Cache
annotation with which the headers are automatically appended to the response.
@GetMapping @Cache(maxAge=60) public List<User> getUsers() { return userService.getAllUsers(); }
For this solution, a corresponding annotation is required first. As an example with some of the usual cache control attributes.
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Documented public @interface Cache { long maxAge() default 0; TimeUnit maxAgeTimeUnit() default TimeUnit.SECONDS; boolean noCache() default false; boolean noStore() default false; boolean cachePrivate() default false; }
A HandlerMethodReturnValueHandler
is implemented to read the annotation from the method and then set the corresponding HTTP headers.
public class CacheControlHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler { private final HandlerMethodReturnValueHandler handler; public CacheControlHandlerMethodReturnValueHandler(HandlerMethodReturnValueHandler handler) { this.handler = handler; } @Override public boolean supportsReturnType(MethodParameter returnType) { return handler.supportsReturnType(returnType); } @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { Cache annotation = AnnotationUtils.findAnnotation(Objects.requireNonNull(returnType.getMethod()), Cache.class); if (annotation == null) { return; } CacheControl cacheControl = getCacheControl(annotation); Optional.ofNullable(webRequest.getNativeResponse(HttpServletResponse.class)) .ifPresent(response -> response.setHeader(HttpHeaders.CACHE_CONTROL, cacheControl.getHeaderValue())); handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } private static CacheControl getCacheControl(Cache annotation) { CacheControl cacheControl; if (annotation.noCache()) { cacheControl = CacheControl.noCache(); } else if (annotation.noStore()) { cacheControl = CacheControl.noStore(); } else if (annotation.maxAge() > 0) { Duration maxAge = Duration.ofSeconds(annotation.maxAgeTimeUnit().toSeconds(annotation.maxAge())); cacheControl = CacheControl.maxAge(maxAge); } else { cacheControl = CacheControl.empty(); } if (annotation.cachePrivate()) { cacheControl.cachePrivate(); } return cacheControl; } }
The attributes of the annotation are used to configure a CachControl
instance as before. Finally, the custom HandlerMethodReturnValueHandler
only needs to be added to the list of configured HandlerMethodReturnValueHandlers
. As the new HandlerMethodReturnValueHandler
only supplements existing ones, it delegates to the existing ones after it has written the Cache-Control
header.
@Configuration public class WebConfig { private final RequestMappingHandlerAdapter handlerAdapter; public WebConfig(RequestMappingHandlerAdapter handlerAdapter) { this.handlerAdapter = handlerAdapter; } @PostConstruct public void init() { List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(handlerAdapter.getReturnValueHandlers()); for (int i = 0; i < handlers.size(); i++) { HandlerMethodReturnValueHandler handler = handlers.get(i); if (handler instanceof HttpEntityMethodProcessor processor) { handlers.set(i, new CacheControlHandlerMethodReturnValueHandler(processor)); } else if (handler instanceof RequestResponseBodyMethodProcessor processor) { handlers.set(i, new CacheControlHandlerMethodReturnValueHandler(processor)); } } handlerAdapter.setReturnValueHandlers(handlers); } }
This completes the annotation-based solution for Cache-Control
headers and caching can be configured without the ResponseEntity
return values.