Provide controller level Cache-Control support
Prior to this commit, Cache-Control HTTP headers could be set using a WebContentInterceptor and configured cache mappings. This commit adds support for cache-related HTTP headers at the controller method level, by returning a ResponseEntity instance: ResponseEntity.status(HttpStatus.OK) .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic()) .eTag("deadb33f8badf00d") .body(entity); Also, this change now automatically checks the "ETag" and "Last-Modified" headers in ResponseEntity, in order to respond HTTP "304 - Not Modified" if necessary. Issue: SPR-8550
This commit is contained in:
parent
38f32e3816
commit
f9ce11eef8
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -59,6 +59,7 @@ import org.springframework.util.ObjectUtils;
|
|||
* </pre>
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Brian Clozel
|
||||
* @since 3.0.2
|
||||
* @see #getStatusCode()
|
||||
*/
|
||||
|
@ -318,6 +319,20 @@ public class ResponseEntity<T> extends HttpEntity<T> {
|
|||
*/
|
||||
B location(URI location);
|
||||
|
||||
/**
|
||||
* Set the caching directives for the resource, as specified by the
|
||||
* {@code Cache-Control} header.
|
||||
*
|
||||
* <p>A {@code CacheControl} instance can be built like
|
||||
* {@code CacheControl.maxAge(3600).cachePublic().noTransform()}.
|
||||
*
|
||||
* @param cacheControl the instance that builds cache related HTTP response headers
|
||||
* @return this builder
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2">RFC-7234 Section 5.2</a>
|
||||
* @since 4.2
|
||||
*/
|
||||
B cacheControl(CacheControl cacheControl);
|
||||
|
||||
/**
|
||||
* Build the response entity with no body.
|
||||
* @return the response entity
|
||||
|
@ -423,6 +438,15 @@ public class ResponseEntity<T> extends HttpEntity<T> {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyBuilder cacheControl(CacheControl cacheControl) {
|
||||
String ccValue = cacheControl.getHeaderValue();
|
||||
if(ccValue != null) {
|
||||
this.headers.setCacheControl(cacheControl.getHeaderValue());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> build() {
|
||||
return new ResponseEntity<Void>(null, this.headers, this.status);
|
||||
|
|
|
@ -19,11 +19,14 @@ package org.springframework.http;
|
|||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
* @author Marcel Overdijk
|
||||
|
@ -163,7 +166,7 @@ public class ResponseEntityTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void headersCopy(){
|
||||
public void headersCopy() {
|
||||
HttpHeaders customHeaders = new HttpHeaders();
|
||||
customHeaders.set("X-CustomHeader", "vale");
|
||||
|
||||
|
@ -178,7 +181,7 @@ public class ResponseEntityTests {
|
|||
}
|
||||
|
||||
@Test // SPR-12792
|
||||
public void headersCopyWithEmptyAndNull(){
|
||||
public void headersCopyWithEmptyAndNull() {
|
||||
ResponseEntity<Void> responseEntityWithEmptyHeaders =
|
||||
ResponseEntity.ok().headers(new HttpHeaders()).build();
|
||||
ResponseEntity<Void> responseEntityWithNullHeaders =
|
||||
|
@ -189,4 +192,58 @@ public class ResponseEntityTests {
|
|||
assertEquals(responseEntityWithEmptyHeaders.toString(), responseEntityWithNullHeaders.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyCacheControl() {
|
||||
|
||||
Integer entity = new Integer(42);
|
||||
|
||||
ResponseEntity<Integer> responseEntity =
|
||||
ResponseEntity.status(HttpStatus.OK)
|
||||
.cacheControl(CacheControl.empty())
|
||||
.body(entity);
|
||||
|
||||
assertNotNull(responseEntity);
|
||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
||||
assertFalse(responseEntity.getHeaders().containsKey(HttpHeaders.CACHE_CONTROL));
|
||||
assertEquals(entity, responseEntity.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheControl() {
|
||||
|
||||
Integer entity = new Integer(42);
|
||||
|
||||
ResponseEntity<Integer> responseEntity =
|
||||
ResponseEntity.status(HttpStatus.OK)
|
||||
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePrivate().
|
||||
mustRevalidate().proxyRevalidate().sMaxAge(30, TimeUnit.MINUTES))
|
||||
.body(entity);
|
||||
|
||||
assertNotNull(responseEntity);
|
||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
||||
assertTrue(responseEntity.getHeaders().containsKey(HttpHeaders.CACHE_CONTROL));
|
||||
assertEquals(entity, responseEntity.getBody());
|
||||
String cacheControlHeader = responseEntity.getHeaders().getCacheControl();
|
||||
assertThat(cacheControlHeader, Matchers.equalTo("max-age=3600, must-revalidate, private, proxy-revalidate, s-maxage=1800"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheControlNoCache() {
|
||||
|
||||
Integer entity = new Integer(42);
|
||||
|
||||
ResponseEntity<Integer> responseEntity =
|
||||
ResponseEntity.status(HttpStatus.OK)
|
||||
.cacheControl(CacheControl.noStore())
|
||||
.body(entity);
|
||||
|
||||
assertNotNull(responseEntity);
|
||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
||||
assertTrue(responseEntity.getHeaders().containsKey(HttpHeaders.CACHE_CONTROL));
|
||||
assertEquals(entity, responseEntity.getBody());
|
||||
|
||||
String cacheControlHeader = responseEntity.getHeaders().getCacheControl();
|
||||
assertThat(cacheControlHeader, Matchers.equalTo("no-store"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -31,6 +31,7 @@ import org.springframework.http.converter.HttpMessageConverter;
|
|||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
|
@ -139,12 +140,36 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
|
|||
}
|
||||
|
||||
Object body = responseEntity.getBody();
|
||||
if (responseEntity instanceof ResponseEntity) {
|
||||
if (isResourceNotModified(webRequest, (ResponseEntity<?>) responseEntity)) {
|
||||
// Ensure headers are flushed, no body should be written
|
||||
outputMessage.flush();
|
||||
// skip call to converters, as they may update the body
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try even with null body. ResponseBodyAdvice could get involved.
|
||||
writeWithMessageConverters(body, returnType, inputMessage, outputMessage);
|
||||
|
||||
// Ensure headers are flushed even if no body was written
|
||||
outputMessage.getBody();
|
||||
outputMessage.flush();
|
||||
}
|
||||
|
||||
private boolean isResourceNotModified(NativeWebRequest webRequest, ResponseEntity<?> responseEntity) {
|
||||
String eTag = responseEntity.getHeaders().getETag();
|
||||
long lastModified = responseEntity.getHeaders().getLastModified();
|
||||
boolean notModified = false;
|
||||
if (lastModified != -1 && StringUtils.hasLength(eTag)) {
|
||||
notModified = webRequest.checkNotModified(eTag, lastModified);
|
||||
}
|
||||
else if (lastModified != -1) {
|
||||
notModified = webRequest.checkNotModified(lastModified);
|
||||
}
|
||||
else if (StringUtils.hasLength(eTag)) {
|
||||
notModified = webRequest.checkNotModified(eTag);
|
||||
}
|
||||
return notModified;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -18,8 +18,12 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
|||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
@ -106,7 +110,7 @@ public class HttpEntityMethodProcessorMockTests {
|
|||
returnTypeInt = new MethodParameter(getClass().getMethod("handle3"), -1);
|
||||
|
||||
mavContainer = new ModelAndViewContainer();
|
||||
servletRequest = new MockHttpServletRequest();
|
||||
servletRequest = new MockHttpServletRequest("GET", "/foo");
|
||||
servletResponse = new MockHttpServletResponse();
|
||||
webRequest = new ServletWebRequest(servletRequest, servletResponse);
|
||||
}
|
||||
|
@ -320,6 +324,98 @@ public class HttpEntityMethodProcessorMockTests {
|
|||
assertEquals("headerValue", outputMessage.getValue().getHeaders().get("header").get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleReturnTypeLastModified() throws Exception {
|
||||
long currentTime = new Date().getTime();
|
||||
long oneMinuteAgo = currentTime - (1000 * 60);
|
||||
servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, currentTime);
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.setDate(HttpHeaders.LAST_MODIFIED, oneMinuteAgo);
|
||||
ResponseEntity<String> returnValue = new ResponseEntity<String>("body", responseHeaders, HttpStatus.OK);
|
||||
|
||||
given(messageConverter.canWrite(String.class, null)).willReturn(true);
|
||||
given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
|
||||
given(messageConverter.canWrite(String.class, MediaType.TEXT_PLAIN)).willReturn(true);
|
||||
|
||||
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
|
||||
|
||||
assertTrue(mavContainer.isRequestHandled());
|
||||
assertEquals(HttpStatus.NOT_MODIFIED.value(), servletResponse.getStatus());
|
||||
assertEquals(oneMinuteAgo/1000 * 1000, Long.parseLong(servletResponse.getHeader(HttpHeaders.LAST_MODIFIED)));
|
||||
assertEquals(0, servletResponse.getContentAsByteArray().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleReturnTypeEtag() throws Exception {
|
||||
String etagValue = "\"deadb33f8badf00d\"";
|
||||
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, etagValue);
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.set(HttpHeaders.ETAG, etagValue);
|
||||
ResponseEntity<String> returnValue = new ResponseEntity<String>("body", responseHeaders, HttpStatus.OK);
|
||||
|
||||
given(messageConverter.canWrite(String.class, null)).willReturn(true);
|
||||
given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
|
||||
given(messageConverter.canWrite(String.class, MediaType.TEXT_PLAIN)).willReturn(true);
|
||||
|
||||
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
|
||||
|
||||
assertTrue(mavContainer.isRequestHandled());
|
||||
assertEquals(HttpStatus.NOT_MODIFIED.value(), servletResponse.getStatus());
|
||||
assertEquals(etagValue, servletResponse.getHeader(HttpHeaders.ETAG));
|
||||
assertEquals(0, servletResponse.getContentAsByteArray().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleReturnTypeETagAndLastModified() throws Exception {
|
||||
long currentTime = new Date().getTime();
|
||||
long oneMinuteAgo = currentTime - (1000 * 60);
|
||||
String etagValue = "\"deadb33f8badf00d\"";
|
||||
servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, currentTime);
|
||||
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, etagValue);
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.setDate(HttpHeaders.LAST_MODIFIED, oneMinuteAgo);
|
||||
responseHeaders.set(HttpHeaders.ETAG, etagValue);
|
||||
ResponseEntity<String> returnValue = new ResponseEntity<String>("body", responseHeaders, HttpStatus.OK);
|
||||
|
||||
given(messageConverter.canWrite(String.class, null)).willReturn(true);
|
||||
given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
|
||||
given(messageConverter.canWrite(String.class, MediaType.TEXT_PLAIN)).willReturn(true);
|
||||
|
||||
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
|
||||
|
||||
assertTrue(mavContainer.isRequestHandled());
|
||||
assertEquals(HttpStatus.NOT_MODIFIED.value(), servletResponse.getStatus());
|
||||
assertEquals(oneMinuteAgo/1000 * 1000, Long.parseLong(servletResponse.getHeader(HttpHeaders.LAST_MODIFIED)));
|
||||
assertEquals(etagValue, servletResponse.getHeader(HttpHeaders.ETAG));
|
||||
assertEquals(0, servletResponse.getContentAsByteArray().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleReturnTypeChangedETagAndLastModified() throws Exception {
|
||||
long currentTime = new Date().getTime();
|
||||
long oneMinuteAgo = currentTime - (1000 * 60);
|
||||
String etagValue = "\"deadb33f8badf00d\"";
|
||||
String changedEtagValue = "\"changed-etag-value\"";
|
||||
servletRequest.addHeader(HttpHeaders.IF_MODIFIED_SINCE, currentTime);
|
||||
servletRequest.addHeader(HttpHeaders.IF_NONE_MATCH, etagValue);
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.setDate(HttpHeaders.LAST_MODIFIED, oneMinuteAgo);
|
||||
responseHeaders.set(HttpHeaders.ETAG, changedEtagValue);
|
||||
ResponseEntity<String> returnValue = new ResponseEntity<String>("body", responseHeaders, HttpStatus.OK);
|
||||
|
||||
given(messageConverter.canWrite(String.class, null)).willReturn(true);
|
||||
given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
|
||||
given(messageConverter.canWrite(String.class, MediaType.TEXT_PLAIN)).willReturn(true);
|
||||
|
||||
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
|
||||
|
||||
assertTrue(mavContainer.isRequestHandled());
|
||||
assertEquals(HttpStatus.OK.value(), servletResponse.getStatus());
|
||||
assertEquals(oneMinuteAgo/1000 * 1000, Long.parseLong(servletResponse.getHeader(HttpHeaders.LAST_MODIFIED)));
|
||||
assertEquals(changedEtagValue, servletResponse.getHeader(HttpHeaders.ETAG));
|
||||
assertEquals(0, servletResponse.getContentAsByteArray().length);
|
||||
}
|
||||
|
||||
public ResponseEntity<String> handle1(HttpEntity<String> httpEntity, ResponseEntity<String> responseEntity, int i, RequestEntity<String> requestEntity) {
|
||||
return responseEntity;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue