From 38f32e38169f7779b9fe20d46b69024039f190ce Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 11 Mar 2015 11:19:52 +0100 Subject: [PATCH] Improve HTTP caching flexiblity This commit improves HTTP caching defaults and flexibility in Spring MVC. 1) Better default caching headers The `WebContentGenerator` abstract class has been updated with better HTTP defaults for HTTP caching, in line with current browsers and proxies implementation (wide support of HTTP1.1, etc); depending on the `setCacheSeconds` value: * sends "Cache-Control: max-age=xxx" for caching responses and do not send a "must-revalidate" value by default. * sends "Cache-Control: no-store" or "Cache-Control: no-cache" in order to prevent caching Other methods used to set specific header such as `setUseExpiresHeader` or `setAlwaysMustRevalidate` are now deprecated in favor of `setCacheControl` for better flexibility. Using one of the deprecated methods re-enables previous HTTP caching behavior. This change is applied in many Handlers, since `WebContentGenerator` is extended by `AbstractController`, `WebContentInterceptor`, `ResourceHttpRequestHandler` and others. 2) New CacheControl builder class This new class brings more flexibility and allows developers to set custom HTTP caching headers. Several strategies are provided: * `CacheControl.maxAge(int)` for caching responses with a "Cache-Control: max-age=xxx" header * `CacheControl.noStore()` prevents responses from being cached with a "Cache-Control: no-store" header * `CacheControl.noCache()` forces caches to revalidate the cached response before reusing it, with a "Cache-Control: no-store" header. From that point, it is possible to chain method calls to craft a custom CacheControl instance: ``` CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS) .cachePublic().noTransform(); ``` 3) Configuring HTTP caching in Resource Handlers On top of the existing ways of configuring caching mechanisms, it is now possible to use a custom `CacheControl` to serve resources: ``` @Configuration public class MyWebConfig extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS); registry.addResourceHandler("/resources/**) .addResourceLocations("classpath:/resources/") .setCacheControl(cc); } } ``` or ``` ``` Issue: SPR-2779, SPR-6834, SPR-7129, SPR-9543, SPR-10464 --- .../springframework/http/CacheControl.java | 294 ++++++++++++++++++ .../http/CacheControlTests.java | 68 ++++ .../config/ResourcesBeanDefinitionParser.java | 42 ++- .../ResourceHandlerRegistration.java | 28 +- .../AbstractHandlerExceptionResolver.java | 11 +- .../web/servlet/mvc/AbstractController.java | 4 +- .../servlet/mvc/WebContentInterceptor.java | 70 ++++- .../AnnotationMethodHandlerAdapter.java | 6 +- .../RequestMappingHandlerAdapter.java | 4 +- .../resource/ResourceHttpRequestHandler.java | 6 +- .../servlet/support/WebContentGenerator.java | 249 +++++++++------ .../view/json/AbstractJackson2View.java | 6 +- .../web/servlet/config/spring-mvc-4.2.xsd | 81 +++++ .../web/servlet/config/MvcNamespaceTests.java | 33 +- .../ResourceHandlerRegistryTests.java | 15 +- .../mvc/WebContentInterceptorTests.java | 15 +- .../RequestMappingHandlerAdapterTests.java | 4 +- .../ResourceHttpRequestHandlerTests.java | 57 +++- .../json/MappingJackson2JsonViewTests.java | 8 +- .../view/xml/MappingJackson2XmlViewTests.java | 8 +- .../mvc-config-resources-chain-no-auto.xml | 1 + 21 files changed, 818 insertions(+), 192 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/CacheControl.java create mode 100644 spring-web/src/test/java/org/springframework/http/CacheControlTests.java diff --git a/spring-web/src/main/java/org/springframework/http/CacheControl.java b/spring-web/src/main/java/org/springframework/http/CacheControl.java new file mode 100644 index 0000000000..28279c855c --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/CacheControl.java @@ -0,0 +1,294 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.http; + +import java.util.concurrent.TimeUnit; + +import org.springframework.util.StringUtils; + +/** + * A builder for creating "Cache-Control" HTTP response headers. + * + *

Adding Cache-Control directives to HTTP responses can significantly improve the client experience when interacting + * with a web application. This builder creates opinionated "Cache-Control" headers with response directives only, with + * several use cases in mind. + * + *

+ * + *

Note that to be efficient, Cache-Control headers should be written along HTTP validators such as + * "Last-Modifed" or "ETag" headers. + * + * @author Brian Clozel + * @see rfc7234 section 5.2.2 + * @see + * HTTP caching - Google developers reference + * @see Mark Nottingham's cache documentation + * @since 4.2 + */ +public class CacheControl { + + private boolean mustRevalidate; + + private boolean noCache; + + private boolean noStore; + + private boolean noTransform; + + private boolean cachePublic; + + private boolean cachePrivate; + + private boolean proxyRevalidate; + + private long maxAge; + + private long sMaxAge; + + /** + * Create a CacheControl instance with default values, + * i.e. that will produce an empty "Cache-Control" header value. + */ + protected CacheControl() { + this.mustRevalidate = false; + this.noCache = false; + this.noStore = false; + this.noTransform = false; + this.cachePublic = false; + this.cachePrivate = false; + this.proxyRevalidate = false; + this.maxAge = -1; + this.sMaxAge = -1; + } + + /** + * Add a "max-age=" directive. + * + *

This directive is well suited for publicly caching resources, knowing that they won't change within + * the configured amount of time. Additional directives can be also used, in case resources shouldn't be + * cached ({@link #cachePrivate()}) or transformed ({@link #noTransform()}) by shared caches. + * + *

In order to prevent caches to reuse the cached response even when it has become stale + * (i.e. the "max-age" delay is passed), the "must-revalidate" directive should be set ({@link #mustRevalidate()} + * + * @param maxAge the maximum time the response should be cached + * @param unit the time unit of the {@code maxAge} argument + * @return {@code this}, to facilitate method chaining + * + * @see rfc7234 section 5.2.2.8 + */ + public static CacheControl maxAge(long maxAge, TimeUnit unit) { + CacheControl cc = new CacheControl(); + cc.maxAge = unit.toSeconds(maxAge); + return cc; + } + + /** + * Add a "no-store" directive + * + *

This directive is well suited for preventing caches (browsers and proxies) to cache the content of responses. + * + * @return {@code this}, to facilitate method chaining + * + * @see rfc7234 section 5.2.2.3 + */ + public static CacheControl noStore() { + CacheControl cc = new CacheControl(); + cc.noStore = true; + return cc; + } + + /** + * Add a "no-cache" directive. + * + *

This directive is well suited for telling caches that the response can be reused only if the client + * revalidates it with the server. This directive won't disable cache altogether and may result with + * clients sending conditional requests (with "ETag", "If-Modified-Since" headers) and the server responding + * with "304 - Not Modified" status. + * + *

In order to disable caching and minimize requests/responses exchanges, the {@link #noStore()} directive + * should be used. + * + * @return {@code this}, to facilitate method chaining + * + * @see rfc7234 section 5.2.2.2 + */ + public static CacheControl noCache() { + CacheControl cc = new CacheControl(); + cc.noCache = true; + return cc; + } + + /** + * Return an empty directive. + * + *

This is well suited for using other optional directives without "no-cache", "no-store" or "max-age". + * + * @return {@code this}, to facilitate method chaining + */ + public static CacheControl empty() { + CacheControl cc = new CacheControl(); + return cc; + } + + /** + * Add a "must-revalidate" directive + * + *

This directive indicates that once it has become stale, a cache MUST NOT use the response + * to satisfy subsequent requests without successful validation on the origin server. + * + * @return {@code this}, to facilitate method chaining + * + * @see rfc7234 section 5.2.2.1 + */ + public CacheControl mustRevalidate() { + this.mustRevalidate = true; + return this; + } + + /** + * Add a "no-transform" directive + * + *

This directive indicates that intermediaries (caches and others) should not transform the response content. + * This can be useful to force caches and CDNs not to automatically gzip or optimize the response content. + * + * @return {@code this}, to facilitate method chaining + * + * @see rfc7234 section 5.2.2.4 + */ + public CacheControl noTransform() { + this.noTransform = true; + return this; + } + + /** + * Add a "public" directive + * + *

This directive indicates that any cache MAY store the response, even if the response + * would normally be non-cacheable or cacheable only within a private cache. + * + * @return {@code this}, to facilitate method chaining + * + * @see rfc7234 section 5.2.2.5 + */ + public CacheControl cachePublic() { + this.cachePublic = true; + return this; + } + + /** + * Add a "private" directive + * + *

This directive indicates that the response message is intended for a single user + * and MUST NOT be stored by a shared cache. + * + * @return {@code this}, to facilitate method chaining + * + * @see rfc7234 section 5.2.2.6 + */ + public CacheControl cachePrivate() { + this.cachePrivate = true; + return this; + } + + /** + * Add a "proxy-revalidate" directive + * + *

This directive has the same meaning as the "must-revalidate" directive, + * except that it does not apply to private caches (i.e. browsers, HTTP clients) + * + * @return {@code this}, to facilitate method chaining + * + * @see rfc7234 section 5.2.2.7 + */ + public CacheControl proxyRevalidate() { + this.proxyRevalidate = true; + return this; + } + + /** + * Add a "s-maxage" directive + * + *

This directive indicates that, in shared caches, the maximum age specified by this directive + * overrides the maximum age specified by other directives. + * + * @param sMaxAge the maximum time the response should be cached + * @param unit the time unit of the {@code sMaxAge} argument + * @return {@code this}, to facilitate method chaining + * + * @see rfc7234 section 5.2.2.9 + */ + public CacheControl sMaxAge(long sMaxAge, TimeUnit unit) { + this.sMaxAge = unit.toSeconds(sMaxAge); + return this; + } + + /** + * Return the "Cache-Control" header value + * + * @return null if no directive was added, the header value otherwise + */ + public String getHeaderValue() { + StringBuilder ccValue = new StringBuilder(); + if (this.maxAge != -1) { + appendDirective(ccValue, "max-age=" + Long.toString(maxAge)); + } + if (this.noCache) { + appendDirective(ccValue, "no-cache"); + } + if (this.noStore) { + appendDirective(ccValue, "no-store"); + } + if (this.mustRevalidate) { + appendDirective(ccValue, "must-revalidate"); + } + if (this.noTransform) { + appendDirective(ccValue, "no-transform"); + } + if (this.cachePublic) { + appendDirective(ccValue, "public"); + } + if (this.cachePrivate) { + appendDirective(ccValue, "private"); + } + if (this.proxyRevalidate) { + appendDirective(ccValue, "proxy-revalidate"); + } + if (this.sMaxAge != -1) { + appendDirective(ccValue, "s-maxage=" + Long.toString(this.sMaxAge)); + } + String ccHeaderValue = ccValue.toString(); + if (StringUtils.hasText(ccHeaderValue)) { + return ccHeaderValue; + } + return null; + } + + private void appendDirective(StringBuilder b, String value) { + if (b.length() > 0) { + b.append(", "); + } + b.append(value); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/CacheControlTests.java b/spring-web/src/test/java/org/springframework/http/CacheControlTests.java new file mode 100644 index 0000000000..66a8a7a4b3 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/CacheControlTests.java @@ -0,0 +1,68 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http; + +import org.hamcrest.Matchers; +import org.junit.Test; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +/** + * @author Brian Clozel + */ +public class CacheControlTests { + + private static final String CACHE_CONTROL_HEADER = "Cache-Control"; + + @Test + public void emptyCacheControl() throws Exception { + CacheControl cc = CacheControl.empty(); + assertThat(cc.getHeaderValue(), Matchers.nullValue()); + } + + @Test + public void maxAge() throws Exception { + CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS); + assertThat(cc.getHeaderValue(), Matchers.equalTo("max-age=3600")); + } + + @Test + public void maxAgeAndDirectives() throws Exception { + CacheControl cc = CacheControl.maxAge(3600, TimeUnit.SECONDS).cachePublic().noTransform(); + assertThat(cc.getHeaderValue(), Matchers.equalTo("max-age=3600, no-transform, public")); + } + + @Test + public void maxAgeAndSMaxAge() throws Exception { + CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).sMaxAge(30, TimeUnit.MINUTES); + assertThat(cc.getHeaderValue(), Matchers.equalTo("max-age=3600, s-maxage=1800")); + } + + @Test + public void noCachePrivate() throws Exception { + CacheControl cc = CacheControl.noCache().cachePrivate(); + assertThat(cc.getHeaderValue(), Matchers.equalTo("no-cache, private")); + } + + @Test + public void noStore() throws Exception { + CacheControl cc = CacheControl.noStore(); + assertThat(cc.getHeaderValue(), Matchers.equalTo("no-store")); + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java index 26322bf7bc..9d00019646 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java @@ -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,6 +18,7 @@ package org.springframework.web.servlet.config; import java.util.Arrays; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.w3c.dom.Element; @@ -34,6 +35,7 @@ import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.core.Ordered; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.handler.MappedInterceptor; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; @@ -162,6 +164,12 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { resourceHandlerDef.getPropertyValues().add("cacheSeconds", cacheSeconds); } + Element cacheControlElement = DomUtils.getChildElementByTagName(element, "cachecontrol"); + if (cacheControlElement != null) { + CacheControl cacheControl = parseCacheControl(cacheControlElement); + resourceHandlerDef.getPropertyValues().add("cacheControl", cacheControl); + } + Element resourceChainElement = DomUtils.getChildElementByTagName(element, "resource-chain"); if (resourceChainElement != null) { parseResourceChain(resourceHandlerDef, parserContext, resourceChainElement, source); @@ -197,6 +205,38 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { } } + private CacheControl parseCacheControl(Element element) { + CacheControl cacheControl = CacheControl.empty(); + if ("true".equals(element.getAttribute("no-cache"))) { + cacheControl = CacheControl.noCache(); + } + else if ("true".equals(element.getAttribute("no-store"))) { + cacheControl = CacheControl.noStore(); + } + else if (element.hasAttribute("max-age")) { + cacheControl = CacheControl.maxAge(Long.parseLong(element.getAttribute("max-age")), TimeUnit.SECONDS); + } + if ("true".equals(element.getAttribute("must-revalidate"))) { + cacheControl = cacheControl.mustRevalidate(); + } + if ("true".equals(element.getAttribute("no-transform"))) { + cacheControl = cacheControl.noTransform(); + } + if ("true".equals(element.getAttribute("cache-public"))) { + cacheControl = cacheControl.cachePublic(); + } + if ("true".equals(element.getAttribute("cache-private"))) { + cacheControl = cacheControl.cachePrivate(); + } + if ("true".equals(element.getAttribute("proxy-revalidate"))) { + cacheControl = cacheControl.proxyRevalidate(); + } + if (element.hasAttribute("s-maxage")) { + cacheControl = cacheControl.sMaxAge(Long.parseLong(element.getAttribute("s-maxage")), TimeUnit.SECONDS); + } + return cacheControl; + } + private void parseResourceCache(ManagedList resourceResolvers, ManagedList resourceTransformers, Element element, Object source) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java index b99730707e..b9f4b6cbf3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java @@ -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. @@ -23,6 +23,7 @@ import org.springframework.cache.Cache; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.resource.PathResourceResolver; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; @@ -45,6 +46,8 @@ public class ResourceHandlerRegistration { private Integer cachePeriod; + private CacheControl cacheControl; + private ResourceChainRegistration resourceChainRegistration; @@ -69,7 +72,7 @@ public class ResourceHandlerRegistration { * {@code /META-INF/public-web-resources/} directory, with resources in the web application root taking precedence. * @return the same {@link ResourceHandlerRegistration} instance for chained method invocation */ - public ResourceHandlerRegistration addResourceLocations(String...resourceLocations) { + public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) { for (String location : resourceLocations) { this.locations.add(resourceLoader.getResource(location)); } @@ -88,6 +91,22 @@ public class ResourceHandlerRegistration { return this; } + /** + * Specify the {@link org.springframework.http.CacheControl} which should be used + * by the the resource handler. + * + *

Setting a custom value here will override the configuration set with {@link #setCachePeriod}. + * + * @param cacheControl the CacheControl configuration to use + * @return the same {@link ResourceHandlerRegistration} instance for chained method invocation + * + * @since 4.2 + */ + public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) { + this.cacheControl = cacheControl; + return this; + } + /** * Configure a chain of resource resolvers and transformers to use. This * can be useful for example to apply a version strategy to resource URLs. @@ -146,7 +165,10 @@ public class ResourceHandlerRegistration { handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers()); } handler.setLocations(this.locations); - if (this.cachePeriod != null) { + if (this.cacheControl != null) { + handler.setCacheControl(this.cacheControl); + } + else if (this.cachePeriod != null) { handler.setCacheSeconds(this.cachePeriod); } return handler; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java index 764bec2c19..4b36302f63 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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. @@ -39,10 +39,6 @@ import org.springframework.web.servlet.ModelAndView; */ public abstract class AbstractHandlerExceptionResolver implements HandlerExceptionResolver, Ordered { - private static final String HEADER_PRAGMA = "Pragma"; - - private static final String HEADER_EXPIRES = "Expires"; - private static final String HEADER_CACHE_CONTROL = "Cache-Control"; @@ -215,13 +211,10 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti /** * Prevents the response from being cached, through setting corresponding - * HTTP headers. See {@code http://www.mnot.net/cache_docs}. + * HTTP {@code Cache-Control: no-store} header. * @param response current HTTP response */ protected void preventCaching(HttpServletResponse response) { - response.setHeader(HEADER_PRAGMA, "no-cache"); - response.setDateHeader(HEADER_EXPIRES, 1L); - response.setHeader(HEADER_CACHE_CONTROL, "no-cache"); response.addHeader(HEADER_CACHE_CONTROL, "no-store"); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java index 772953ffee..a0eeec2a7c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java @@ -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. @@ -130,7 +130,7 @@ public abstract class AbstractController extends WebContentGenerator implements throws Exception { // Delegate to WebContentGenerator for checking and preparing. - checkAndPrepare(request, response, this instanceof LastModified); + checkAndPrepare(request, response); // Execute handleRequestInternal in synchronized block if required. if (this.synchronizeOnSession) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java index f9c23f84dc..be33c5f07b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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. @@ -20,6 +20,8 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; +import java.util.concurrent.TimeUnit; + import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -27,6 +29,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.support.WebContentGenerator; @@ -34,7 +37,7 @@ import org.springframework.web.util.UrlPathHelper; /** * Interceptor that checks and prepares request and response. Checks for supported - * methods and a required session, and applies the specified number of cache seconds. + * methods and a required session, and applies the specified {@link org.springframework.http.CacheControl}. * See superclass bean properties for configuration options. * *

All the settings supported by this interceptor can also be set on AbstractController. @@ -42,6 +45,7 @@ import org.springframework.web.util.UrlPathHelper; * controllers mapped by a HandlerMapping. * * @author Juergen Hoeller + * @author Brian Clozel * @since 27.11.2003 * @see AbstractController */ @@ -49,10 +53,9 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle private UrlPathHelper urlPathHelper = new UrlPathHelper(); - private Map cacheMappings = new HashMap(); - private PathMatcher pathMatcher = new AntPathMatcher(); + private Map cacheMappings = new HashMap(); public WebContentInterceptor() { // no restriction of HTTP methods by default, @@ -120,14 +123,47 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle Enumeration propNames = cacheMappings.propertyNames(); while (propNames.hasMoreElements()) { String path = (String) propNames.nextElement(); - this.cacheMappings.put(path, Integer.valueOf(cacheMappings.getProperty(path))); + int cacheSeconds = Integer.valueOf(cacheMappings.getProperty(path)); + if (cacheSeconds > 0) { + this.cacheMappings.put(path, CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS)); + } + else if (cacheSeconds == 0) { + this.cacheMappings.put(path, CacheControl.noStore()); + } + else { + this.cacheMappings.put(path, CacheControl.empty()); + } } } + /** + * Map specific URL paths to a specific {@link org.springframework.http.CacheControl}. + *

Overrides the default cache seconds setting of this interceptor. + * Can specify a empty {@link org.springframework.http.CacheControl} instance + * to exclude a URL path from default caching. + * + *

Supports direct matches, e.g. a registered "/test" matches "/test", + * and a various Ant-style pattern matches, e.g. a registered "/t*" matches + * both "/test" and "/team". For details, see the AntPathMatcher javadoc. + * + * @param cacheControl the {@code CacheControl} to use + * @param paths URL paths that will map to the given {@code CacheControl} + * @see #setCacheSeconds + * @see org.springframework.util.AntPathMatcher + * @since 4.2 + */ + public void addCacheMapping(CacheControl cacheControl, String... paths) { + for (String path : paths) { + this.cacheMappings.put(path, cacheControl); + } + } + + /** * Set the PathMatcher implementation to use for matching URL paths * against registered URL patterns, for determining cache mappings. * Default is AntPathMatcher. + * @see #addCacheMapping * @see #setCacheMappings * @see org.springframework.util.AntPathMatcher */ @@ -146,44 +182,44 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle logger.debug("Looking up cache seconds for [" + lookupPath + "]"); } - Integer cacheSeconds = lookupCacheSeconds(lookupPath); - if (cacheSeconds != null) { + CacheControl cacheControl = lookupCacheSeconds(lookupPath); + if (cacheControl != null) { if (logger.isDebugEnabled()) { - logger.debug("Applying " + cacheSeconds + " cache seconds to [" + lookupPath + "]"); + logger.debug("Applying CacheControl to [" + lookupPath + "]"); } - checkAndPrepare(request, response, cacheSeconds, handler instanceof LastModified); + checkAndPrepare(request, response, cacheControl); } else { if (logger.isDebugEnabled()) { logger.debug("Applying default cache seconds to [" + lookupPath + "]"); } - checkAndPrepare(request, response, handler instanceof LastModified); + checkAndPrepare(request, response); } return true; } /** - * Look up a cache seconds value for the given URL path. + * Look up a {@link org.springframework.http.CacheControl} instance for the given URL path. *

Supports direct matches, e.g. a registered "/test" matches "/test", * and various Ant-style pattern matches, e.g. a registered "/t*" matches * both "/test" and "/team". For details, see the AntPathMatcher class. * @param urlPath URL the bean is mapped to - * @return the associated cache seconds, or {@code null} if not found + * @return the associated {@code CacheControl}, or {@code null} if not found * @see org.springframework.util.AntPathMatcher */ - protected Integer lookupCacheSeconds(String urlPath) { + protected CacheControl lookupCacheSeconds(String urlPath) { // direct match? - Integer cacheSeconds = this.cacheMappings.get(urlPath); - if (cacheSeconds == null) { + CacheControl cacheControl = this.cacheMappings.get(urlPath); + if (cacheControl == null) { // pattern match? for (String registeredPath : this.cacheMappings.keySet()) { if (this.pathMatcher.match(registeredPath, urlPath)) { - cacheSeconds = this.cacheMappings.get(registeredPath); + cacheControl = this.cacheMappings.get(registeredPath); } } } - return cacheSeconds; + return cacheControl; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index 6c071af07f..a4c20452c1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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. @@ -412,12 +412,12 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator if (annotatedWithSessionAttributes) { // Always prevent caching in case of session attribute management. - checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true); + checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers); // Prepare cached set of session attributes names. } else { // Uses configured default cacheSeconds setting. - checkAndPrepare(request, response, true); + checkAndPrepare(request, response); } // Execute invokeHandlerMethod in synchronized block if required. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index f53d0b3fbc..b0ab919083 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -685,11 +685,11 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { // Always prevent caching in case of session attribute management. - checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true); + checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers); } else { // Uses configured default cacheSeconds setting. - checkAndPrepare(request, response, true); + checkAndPrepare(request, response); } // Execute invokeHandlerMethod in synchronized block if required. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index b2e721f04d..4de612ee91 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -60,8 +60,8 @@ import org.springframework.web.servlet.support.WebContentGenerator; *

The {@linkplain #setLocations "locations" property} takes a list of Spring {@link Resource} * locations from which static resources are allowed to be served by this handler. For a given request, * the list of locations will be consulted in order for the presence of the requested resource, and the - * first found match will be written to the response, with {@code Expires} and {@code Cache-Control} - * headers set as configured. The handler also properly evaluates the {@code Last-Modified} header + * first found match will be written to the response, with a HTTP Caching headers + * set as configured. The handler also properly evaluates the {@code Last-Modified} header * (if present) so that a {@code 304} status code will be returned as appropriate, avoiding unnecessary * overhead for resources that are already cached by the client. The use of {@code Resource} locations * allows resource requests to easily be mapped to locations other than the web application root. @@ -210,7 +210,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - checkAndPrepare(request, response, true); + checkAndPrepare(request, response); // check whether a matching resource exists Resource resource = getResource(request); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java index 087bcc0f85..42e1538113 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java @@ -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. @@ -19,6 +19,8 @@ package org.springframework.web.servlet.support; import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.TimeUnit; + import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -26,7 +28,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.HttpSessionRequiredException; -import org.springframework.web.context.request.WebRequest; +import org.springframework.http.CacheControl; import org.springframework.web.context.support.WebApplicationObjectSupport; /** @@ -37,11 +39,17 @@ import org.springframework.web.context.support.WebApplicationObjectSupport; * {@link org.springframework.web.servlet.HandlerAdapter}. * *

Supports HTTP cache control options. The usage of corresponding - * HTTP headers can be controlled via the "useExpiresHeader", - * "useCacheControlHeader" and "useCacheControlNoStore" properties. + * HTTP headers can be controlled via the "setCacheSeconds" or "setCacheControl" properties. + * As of 4.2, its default behavior changed when using only {@link #setCacheSeconds(int)}, sending + * HTTP response headers that are more in line with current browsers and proxies implementations. + * + *

Reverting to the previous behavior can be easily done by using one of the nealy deprecated methods + * {@link #setUseExpiresHeader}, {@link #setUseCacheControlHeader}, {@link #setUseCacheControlNoStore} or + * {@link #setAlwaysMustRevalidate}. * * @author Rod Johnson * @author Juergen Hoeller + * @author Brian Clozel * @see #setCacheSeconds * @see #setRequireSession */ @@ -56,7 +64,6 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { /** HTTP method "POST" */ public static final String METHOD_POST = "POST"; - private static final String HEADER_PRAGMA = "Pragma"; private static final String HEADER_EXPIRES = "Expires"; @@ -65,10 +72,12 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { /** Set of supported HTTP methods */ - private Set supportedMethods; + private Set supportedMethods; private boolean requireSession = false; + private int cacheSeconds = -1; + /** Use HTTP 1.0 expires header? */ private boolean useExpiresHeader = true; @@ -78,10 +87,12 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { /** Use HTTP 1.1 cache-control header value "no-store"? */ private boolean useCacheControlNoStore = true; - private int cacheSeconds = -1; - private boolean alwaysMustRevalidate = false; + private boolean usePreviousHttpCachingBehavior = false; + + private CacheControl cacheControl; + /** * Create a new WebContentGenerator which supports @@ -151,17 +162,42 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { } /** - * Set whether to use the HTTP 1.0 expires header. Default is "true". + * Set the {@link org.springframework.http.CacheControl} instance to build + * the Cache-Control HTTP response header. + * + * @since 4.2 + */ + public void setCacheControl(CacheControl cacheControl) { + this.cacheControl = cacheControl; + } + + /** + * Get the {@link org.springframework.http.CacheControl} instance + * that builds the Cache-Control HTTP response header. + * + * @since 4.2 + */ + public CacheControl getCacheControl() { + return cacheControl; + } + + /** + * Set whether to use the HTTP 1.0 expires header. Default is "false". *

Note: Cache headers will only get applied if caching is enabled * (or explicitly prevented) for the current request. + * + * @deprecated in favor of {@link #setCacheSeconds} or {@link #setCacheControl}. */ + @Deprecated public final void setUseExpiresHeader(boolean useExpiresHeader) { this.useExpiresHeader = useExpiresHeader; + this.usePreviousHttpCachingBehavior = true; } /** * Return whether the HTTP 1.0 expires header is used. */ + @Deprecated public final boolean isUseExpiresHeader() { return this.useExpiresHeader; } @@ -170,14 +206,19 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { * Set whether to use the HTTP 1.1 cache-control header. Default is "true". *

Note: Cache headers will only get applied if caching is enabled * (or explicitly prevented) for the current request. + * + * @deprecated in favor of {@link #setCacheSeconds} or {@link #setCacheControl}. */ + @Deprecated public final void setUseCacheControlHeader(boolean useCacheControlHeader) { this.useCacheControlHeader = useCacheControlHeader; + this.usePreviousHttpCachingBehavior = true; } /** * Return whether the HTTP 1.1 cache-control header is used. */ + @Deprecated public final boolean isUseCacheControlHeader() { return this.useCacheControlHeader; } @@ -185,14 +226,19 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { /** * Set whether to use the HTTP 1.1 cache-control header value "no-store" * when preventing caching. Default is "true". + * + * @deprecated in favor of {@link #setCacheSeconds} or {@link #setCacheControl}. */ + @Deprecated public final void setUseCacheControlNoStore(boolean useCacheControlNoStore) { this.useCacheControlNoStore = useCacheControlNoStore; + this.usePreviousHttpCachingBehavior = true; } /** * Return whether the HTTP 1.1 cache-control header value "no-store" is used. */ + @Deprecated public final boolean isUseCacheControlNoStore() { return this.useCacheControlNoStore; } @@ -201,30 +247,46 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { * An option to add 'must-revalidate' to every Cache-Control header. This * may be useful with annotated controller methods, which can * programmatically do a lastModified calculation as described in - * {@link WebRequest#checkNotModified(long)}. Default is "false", - * effectively relying on whether the handler implements - * {@link org.springframework.web.servlet.mvc.LastModified} or not. + * {@link WebRequest#checkNotModified(long)}. Default is "false". + * + * @deprecated in favor of {@link #setCacheSeconds} or {@link #setCacheControl}. */ + @Deprecated public void setAlwaysMustRevalidate(boolean mustRevalidate) { this.alwaysMustRevalidate = mustRevalidate; + this.usePreviousHttpCachingBehavior = true; } /** * Return whether 'must-revalidate' is added to every Cache-Control header. */ + @Deprecated public boolean isAlwaysMustRevalidate() { return alwaysMustRevalidate; } /** - * Cache content for the given number of seconds. Default is -1, - * indicating no generation of cache-related headers. - *

Only if this is set to 0 (no cache) or a positive value (cache for - * this many seconds) will this class generate cache headers. - *

The headers can be overwritten by subclasses, before content is generated. + * Cache content for the given number of seconds, by writing + * cache-related HTTP headers to the response: + *

+ *

For more specific needs, a custom {@link org.springframework.http.CacheControl} should be used. + * + * @see #setCacheControl */ public final void setCacheSeconds(int seconds) { this.cacheSeconds = seconds; + if (!this.usePreviousHttpCachingBehavior) { + if (cacheSeconds > 0) { + this.cacheControl = CacheControl.maxAge(seconds, TimeUnit.SECONDS); + } + else if (cacheSeconds == 0) { + this.cacheControl = CacheControl.noStore(); + } + } } /** @@ -241,29 +303,45 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { * and applies the number of cache seconds specified for this generator. * @param request current HTTP request * @param response current HTTP response - * @param lastModified if the mapped handler provides Last-Modified support * @throws ServletException if the request cannot be handled because a check failed */ protected final void checkAndPrepare( - HttpServletRequest request, HttpServletResponse response, boolean lastModified) + HttpServletRequest request, HttpServletResponse response) throws ServletException { - checkAndPrepare(request, response, this.cacheSeconds, lastModified); + checkAndPrepare(request, response, this.cacheControl); + } + + protected final void checkAndPrepare( + HttpServletRequest request, HttpServletResponse response, int cacheSeconds) throws ServletException { + + CacheControl cControl; + if (cacheSeconds > 0) { + cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS); + } + else if (cacheSeconds == 0) { + cControl = CacheControl.noStore(); + } + else { + cControl = CacheControl.empty(); + } + checkAndPrepare(request, response, cControl); } /** * Check and prepare the given request and response according to the settings - * of this generator. Checks for supported methods and a required session, - * and applies the given number of cache seconds. + * of this generator. Checks for supported methods and a required session + * specified for this generator. Also applies the {@link org.springframework.http.CacheControl} + * given as a parameter. * @param request current HTTP request * @param response current HTTP response - * @param cacheSeconds positive number of seconds into the future that the - * response should be cacheable for, 0 to prevent caching - * @param lastModified if the mapped handler provides Last-Modified support + * @param cacheControl the {@link org.springframework.http.CacheControl} to use * @throws ServletException if the request cannot be handled because a check failed + * + * @since 4.2 */ protected final void checkAndPrepare( - HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified) + HttpServletRequest request, HttpServletResponse response, CacheControl cacheControl) throws ServletException { // Check whether we should support the request method. @@ -280,9 +358,49 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { } } - // Do declarative cache control. - // Revalidate if the controller supports last-modified. - applyCacheSeconds(response, cacheSeconds, lastModified); + if (this.usePreviousHttpCachingBehavior) { + addHttp10CacheHeaders(response); + } + else if (cacheControl != null) { + String ccValue = cacheControl.getHeaderValue(); + if (ccValue != null) { + response.setHeader(HEADER_CACHE_CONTROL, ccValue); + } + } + } + + protected void addHttp10CacheHeaders(HttpServletResponse response) { + if (this.cacheSeconds > 0) { + cacheForSeconds(response, this.cacheSeconds, this.alwaysMustRevalidate); + } + else if (this.cacheSeconds == 0) { + preventCaching(response); + } + } + + /** + * Set HTTP headers to allow caching for the given number of seconds. + * Tells the browser to revalidate the resource if mustRevalidate is + * {@code true}. + * @param response the current HTTP response + * @param seconds number of seconds into the future that the response + * should be cacheable for + * @param mustRevalidate whether the client should revalidate the resource + * (typically only necessary for controllers with last-modified support) + */ + protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) { + if (this.useExpiresHeader) { + // HTTP 1.0 header + response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L); + } + if (this.useCacheControlHeader) { + // HTTP 1.1 header + String headerValue = "max-age=" + seconds; + if (mustRevalidate) { + headerValue += ", must-revalidate"; + } + response.setHeader(HEADER_CACHE_CONTROL, headerValue); + } } /** @@ -305,77 +423,4 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { } } - /** - * Set HTTP headers to allow caching for the given number of seconds. - * Does not tell the browser to revalidate the resource. - * @param response current HTTP response - * @param seconds number of seconds into the future that the response - * should be cacheable for - * @see #cacheForSeconds(javax.servlet.http.HttpServletResponse, int, boolean) - */ - protected final void cacheForSeconds(HttpServletResponse response, int seconds) { - cacheForSeconds(response, seconds, false); - } - - /** - * Set HTTP headers to allow caching for the given number of seconds. - * Tells the browser to revalidate the resource if mustRevalidate is - * {@code true}. - * @param response the current HTTP response - * @param seconds number of seconds into the future that the response - * should be cacheable for - * @param mustRevalidate whether the client should revalidate the resource - * (typically only necessary for controllers with last-modified support) - */ - protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) { - if (this.useExpiresHeader) { - // HTTP 1.0 header - response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L); - } - if (this.useCacheControlHeader) { - // HTTP 1.1 header - String headerValue = "max-age=" + seconds; - if (mustRevalidate || this.alwaysMustRevalidate) { - headerValue += ", must-revalidate"; - } - response.setHeader(HEADER_CACHE_CONTROL, headerValue); - } - } - - /** - * Apply the given cache seconds and generate corresponding HTTP headers, - * i.e. allow caching for the given number of seconds in case of a positive - * value, prevent caching if given a 0 value, do nothing else. - * Does not tell the browser to revalidate the resource. - * @param response current HTTP response - * @param seconds positive number of seconds into the future that the - * response should be cacheable for, 0 to prevent caching - * @see #cacheForSeconds(javax.servlet.http.HttpServletResponse, int, boolean) - */ - protected final void applyCacheSeconds(HttpServletResponse response, int seconds) { - applyCacheSeconds(response, seconds, false); - } - - /** - * Apply the given cache seconds and generate respective HTTP headers. - *

That is, allow caching for the given number of seconds in the - * case of a positive value, prevent caching if given a 0 value, else - * do nothing (i.e. leave caching to the client). - * @param response the current HTTP response - * @param seconds the (positive) number of seconds into the future that - * the response should be cacheable for; 0 to prevent caching; and - * a negative value to leave caching to the client. - * @param mustRevalidate whether the client should revalidate the resource - * (typically only necessary for controllers with last-modified support) - */ - protected final void applyCacheSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) { - if (seconds > 0) { - cacheForSeconds(response, seconds, mustRevalidate); - } - else if (seconds == 0) { - preventCaching(response); - } - // Leave caching to the client otherwise. - } - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java index 3ce9ecdfb1..f326c718c8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java @@ -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. @@ -150,9 +150,7 @@ public abstract class AbstractJackson2View extends AbstractView { setResponseContentType(request, response); response.setCharacterEncoding(this.encoding.getJavaName()); if (this.disableCaching) { - response.addHeader("Pragma", "no-cache"); - response.addHeader("Cache-Control", "no-cache, no-store, max-age=0"); - response.addDateHeader("Expires", 1L); + response.addHeader("Cache-Control", "no-store"); } } diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd index 7f615bdd62..39570b69ed 100644 --- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd +++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd @@ -477,6 +477,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index e263839be5..f9cbb1e147 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -29,6 +29,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.TimeUnit; import javax.servlet.RequestDispatcher; import javax.validation.constraints.NotNull; @@ -73,6 +74,7 @@ import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.http.CacheControl; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; @@ -202,7 +204,7 @@ public class MvcNamespaceTests { assertTrue(converters.size() > 0); for (HttpMessageConverter converter : converters) { if (converter instanceof AbstractJackson2HttpMessageConverter) { - ObjectMapper objectMapper = ((AbstractJackson2HttpMessageConverter)converter).getObjectMapper(); + ObjectMapper objectMapper = ((AbstractJackson2HttpMessageConverter) converter).getObjectMapper(); assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)); assertFalse(objectMapper.getSerializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)); assertFalse(objectMapper.getDeserializationConfig().isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)); @@ -233,7 +235,7 @@ public class MvcNamespaceTests { adapter.handle(request, response, handlerMethod); assertTrue(handler.recordedValidationError); assertEquals(LocalDate.parse("2009-10-31").toDate(), handler.date); - assertEquals(Double.valueOf(0.9999),handler.percent); + assertEquals(Double.valueOf(0.9999), handler.percent); CompositeUriComponentsContributor uriComponentsContributor = this.appContext.getBean( MvcUriComponentsBuilder.MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME, @@ -242,7 +244,7 @@ public class MvcNamespaceTests { assertNotNull(uriComponentsContributor); } - @Test(expected=TypeMismatchException.class) + @Test(expected = TypeMismatchException.class) public void testCustomConversionService() throws Exception { loadBeanDefinitions("mvc-config-custom-conversion-service.xml", 13); @@ -384,10 +386,12 @@ public class MvcNamespaceTests { assertEquals(5, mapping.getOrder()); assertNotNull(mapping.getUrlMap().get("/resources/**")); - ResourceHttpRequestHandler handler = appContext.getBean((String)mapping.getUrlMap().get("/resources/**"), + ResourceHttpRequestHandler handler = appContext.getBean((String) mapping.getUrlMap().get("/resources/**"), ResourceHttpRequestHandler.class); assertNotNull(handler); assertEquals(3600, handler.getCacheSeconds()); + assertThat(handler.getCacheControl().getHeaderValue(), + Matchers.equalTo(CacheControl.maxAge(1, TimeUnit.HOURS).getHeaderValue())); } @Test @@ -397,7 +401,7 @@ public class MvcNamespaceTests { SimpleUrlHandlerMapping mapping = appContext.getBean(SimpleUrlHandlerMapping.class); assertNotNull(mapping); assertNotNull(mapping.getUrlMap().get("/resources/**")); - ResourceHttpRequestHandler handler = appContext.getBean((String)mapping.getUrlMap().get("/resources/**"), + ResourceHttpRequestHandler handler = appContext.getBean((String) mapping.getUrlMap().get("/resources/**"), ResourceHttpRequestHandler.class); assertNotNull(handler); @@ -435,10 +439,14 @@ public class MvcNamespaceTests { SimpleUrlHandlerMapping mapping = appContext.getBean(SimpleUrlHandlerMapping.class); assertNotNull(mapping); assertNotNull(mapping.getUrlMap().get("/resources/**")); - ResourceHttpRequestHandler handler = appContext.getBean((String)mapping.getUrlMap().get("/resources/**"), + ResourceHttpRequestHandler handler = appContext.getBean((String) mapping.getUrlMap().get("/resources/**"), ResourceHttpRequestHandler.class); assertNotNull(handler); + assertThat(handler.getCacheControl().getHeaderValue(), + Matchers.equalTo(CacheControl.maxAge(1, TimeUnit.HOURS) + .sMaxAge(30, TimeUnit.MINUTES).cachePublic().getHeaderValue())); + List resolvers = handler.getResourceResolvers(); assertThat(resolvers, Matchers.hasSize(3)); assertThat(resolvers.get(0), Matchers.instanceOf(VersionResourceResolver.class)); @@ -762,12 +770,12 @@ public class MvcNamespaceTests { }; accessor = new DirectFieldAccessor(tilesConfigurer); assertArrayEquals(definitions, (String[]) accessor.getPropertyValue("definitions")); - assertTrue((boolean)accessor.getPropertyValue("checkRefresh")); + assertTrue((boolean) accessor.getPropertyValue("checkRefresh")); FreeMarkerConfigurer freeMarkerConfigurer = appContext.getBean(FreeMarkerConfigurer.class); assertNotNull(freeMarkerConfigurer); accessor = new DirectFieldAccessor(freeMarkerConfigurer); - assertArrayEquals(new String[]{"/", "/test"}, (String[]) accessor.getPropertyValue("templateLoaderPaths")); + assertArrayEquals(new String[] {"/", "/test"}, (String[]) accessor.getPropertyValue("templateLoaderPaths")); VelocityConfigurer velocityConfigurer = appContext.getBean(VelocityConfigurer.class); assertNotNull(velocityConfigurer); @@ -846,7 +854,7 @@ public class MvcNamespaceTests { } - @DateTimeFormat(iso=ISO.DATE) + @DateTimeFormat(iso = ISO.DATE) @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface IsoDate { @@ -868,7 +876,9 @@ public class MvcNamespaceTests { public static class TestController { private Date date; + private Double percent; + private boolean recordedValidationError; @RequestMapping @@ -900,7 +910,7 @@ public class MvcNamespaceTests { private static class TestBean { - @NotNull(groups=MyGroup.class) + @NotNull(groups = MyGroup.class) private String field; @SuppressWarnings("unused") @@ -920,7 +930,8 @@ public class MvcNamespaceTests { public RequestDispatcher getNamedDispatcher(String path) { if (path.equals("default") || path.equals("custom")) { return new MockRequestDispatcher("/"); - } else { + } + else { return null; } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java index 7cc4f28900..101a3135e1 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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. @@ -27,6 +27,7 @@ import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.mock.web.test.MockServletContext; +import org.springframework.http.CacheControl; import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; @@ -88,6 +89,18 @@ public class ResourceHandlerRegistryTests { this.registration.setCachePeriod(0); assertEquals(0, getHandler("/resources/**").getCacheSeconds()); + assertThat(getHandler("/resources/**").getCacheControl().getHeaderValue(), + Matchers.equalTo(CacheControl.noStore().getHeaderValue())); + } + + @Test + public void cacheControl() { + assertThat(getHandler("/resources/**").getCacheControl(), + Matchers.nullValue()); + + this.registration.setCacheControl(CacheControl.noCache().cachePrivate()); + assertThat(getHandler("/resources/**").getCacheControl().getHeaderValue(), + Matchers.equalTo(CacheControl.noCache().cachePrivate().getHeaderValue())); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java index c9251ac642..acbce4a2bc 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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. @@ -53,9 +53,6 @@ public class WebContentInterceptorTests { interceptor.preHandle(request, response, null); - List expiresHeaders = response.getHeaders("Expires"); - assertNotNull("'Expires' header not set (must be) : null", expiresHeaders); - assertTrue("'Expires' header not set (must be) : empty", expiresHeaders.size() > 0); List cacheControlHeaders = response.getHeaders("Cache-Control"); assertNotNull("'Cache-Control' header not set (must be) : null", cacheControlHeaders); assertTrue("'Cache-Control' header not set (must be) : empty", cacheControlHeaders.size() > 0); @@ -73,17 +70,12 @@ public class WebContentInterceptorTests { request.setRequestURI("http://localhost:7070/example/adminhandle.vm"); interceptor.preHandle(request, response, null); - List expiresHeaders = response.getHeaders("Expires"); - assertSame("'Expires' header set (must not be) : empty", 0, expiresHeaders.size()); List cacheControlHeaders = response.getHeaders("Cache-Control"); - assertSame("'Cache-Control' header set (must not be) : empty", 0, cacheControlHeaders.size()); + assertSame("'Cache-Control' header set must be empty", 0, cacheControlHeaders.size()); request.setRequestURI("http://localhost:7070/example/bingo.html"); interceptor.preHandle(request, response, null); - expiresHeaders = response.getHeaders("Expires"); - assertNotNull("'Expires' header not set (must be) : null", expiresHeaders); - assertTrue("'Expires' header not set (must be) : empty", expiresHeaders.size() > 0); cacheControlHeaders = response.getHeaders("Cache-Control"); assertNotNull("'Cache-Control' header not set (must be) : null", cacheControlHeaders); assertTrue("'Cache-Control' header not set (must be) : empty", cacheControlHeaders.size() > 0); @@ -96,9 +88,6 @@ public class WebContentInterceptorTests { interceptor.preHandle(request, response, null); - List expiresHeaders = response.getHeaders("Expires"); - assertNotNull("'Expires' header not set (must be) : null", expiresHeaders); - assertTrue("'Expires' header not set (must be) : empty", expiresHeaders.size() > 0); List cacheControlHeaders = response.getHeaders("Cache-Control"); assertNotNull("'Cache-Control' header not set (must be) : null", cacheControlHeaders); assertTrue("'Cache-Control' header not set (must be) : empty", cacheControlHeaders.size() > 0); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java index dde3afccae..e0ad67ad63 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java @@ -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. @@ -118,7 +118,7 @@ public class RequestMappingHandlerAdapterTests { this.handlerAdapter.afterPropertiesSet(); this.handlerAdapter.handle(this.request, this.response, handlerMethod(handler, "handle")); - assertEquals("no-cache", this.response.getHeader("Cache-Control")); + assertEquals("no-store", this.response.getHeader("Cache-Control")); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java index b7129b7501..b2bb91be40 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java @@ -16,13 +16,16 @@ package org.springframework.web.servlet.resource; +import static org.junit.Assert.*; + import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; + import javax.servlet.http.HttpServletResponse; -import static org.junit.Assert.*; +import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; @@ -75,21 +78,62 @@ public class ResourceHttpRequestHandlerTests { assertEquals("text/css", this.response.getContentType()); assertEquals(17, this.response.getContentLength()); - assertTrue(headerAsLong("Expires") >= System.currentTimeMillis() - 1000 + (3600 * 1000)); - assertEquals("max-age=3600, must-revalidate", this.response.getHeader("Cache-Control")); + assertEquals("max-age=3600", this.response.getHeader("Cache-Control")); assertTrue(this.response.containsHeader("Last-Modified")); assertEquals(headerAsLong("Last-Modified"), resourceLastModified("test/foo.css")); assertEquals("h1 { color:red; }", this.response.getContentAsString()); } + @Test + public void getResourceNoCache() throws Exception { + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); + this.handler.setCacheSeconds(0); + this.handler.handleRequest(this.request, this.response); + + assertEquals("no-store", this.response.getHeader("Cache-Control")); + assertTrue(this.response.containsHeader("Last-Modified")); + assertEquals(headerAsLong("Last-Modified"), resourceLastModified("test/foo.css")); + } + + @Test + @SuppressWarnings("deprecation") + public void getResourcePreviousBehaviorCache() throws Exception { + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); + this.handler.setCacheSeconds(3600); + this.handler.setUseExpiresHeader(true); + this.handler.setUseCacheControlHeader(true); + this.handler.setAlwaysMustRevalidate(true); + this.handler.handleRequest(this.request, this.response); + + assertEquals("max-age=3600, must-revalidate", this.response.getHeader("Cache-Control")); + assertTrue(headerAsLong("Expires") >= System.currentTimeMillis() - 1000 + (3600 * 1000)); + assertTrue(this.response.containsHeader("Last-Modified")); + assertEquals(headerAsLong("Last-Modified"), resourceLastModified("test/foo.css")); + } + + @Test + @SuppressWarnings("deprecation") + public void getResourcePreviousBehaviorNoCache() throws Exception { + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); + this.handler.setCacheSeconds(0); + this.handler.setUseCacheControlNoStore(true); + this.handler.setUseCacheControlHeader(true); + this.handler.handleRequest(this.request, this.response); + + assertEquals("no-cache", this.response.getHeader("Pragma")); + assertThat(this.response.getHeaderValues("Cache-Control"), Matchers.contains("no-cache", "no-store")); + assertTrue(headerAsLong("Expires") == 1); + assertTrue(this.response.containsHeader("Last-Modified")); + assertEquals(headerAsLong("Last-Modified"), resourceLastModified("test/foo.css")); + } + @Test public void getResourceWithHtmlMediaType() throws Exception { this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.html"); this.handler.handleRequest(this.request, this.response); assertEquals("text/html", this.response.getContentType()); - assertTrue(headerAsLong("Expires") >= System.currentTimeMillis() - 1000 + (3600 * 1000)); - assertEquals("max-age=3600, must-revalidate", this.response.getHeader("Cache-Control")); + assertEquals("max-age=3600", this.response.getHeader("Cache-Control")); assertTrue(this.response.containsHeader("Last-Modified")); assertEquals(headerAsLong("Last-Modified"), resourceLastModified("test/foo.html")); } @@ -101,8 +145,7 @@ public class ResourceHttpRequestHandlerTests { assertEquals("text/css", this.response.getContentType()); assertEquals(17, this.response.getContentLength()); - assertTrue(headerAsLong("Expires") >= System.currentTimeMillis() - 1000 + (3600 * 1000)); - assertEquals("max-age=3600, must-revalidate", this.response.getHeader("Cache-Control")); + assertEquals("max-age=3600", this.response.getHeader("Cache-Control")); assertTrue(this.response.containsHeader("Last-Modified")); assertEquals(headerAsLong("Last-Modified"), resourceLastModified("testalternatepath/baz.css")); assertEquals("h1 { color:red; }", this.response.getContentAsString()); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java index 94604f7331..c57ed13f5b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java @@ -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. @@ -97,9 +97,7 @@ public class MappingJackson2JsonViewTests { view.setUpdateContentLength(true); view.render(model, request, response); - assertEquals("no-cache", response.getHeader("Pragma")); - assertEquals("no-cache, no-store, max-age=0", response.getHeader("Cache-Control")); - assertNotNull(response.getHeader("Expires")); + assertEquals("no-store", response.getHeader("Cache-Control")); assertEquals(MappingJackson2JsonView.DEFAULT_CONTENT_TYPE, response.getContentType()); @@ -134,9 +132,7 @@ public class MappingJackson2JsonViewTests { view.render(model, request, response); - assertNull(response.getHeader("Pragma")); assertNull(response.getHeader("Cache-Control")); - assertNull(response.getHeader("Expires")); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlViewTests.java index c5965d0e65..1b9e6adce8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlViewTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlViewTests.java @@ -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. @@ -90,9 +90,7 @@ public class MappingJackson2XmlViewTests { view.setUpdateContentLength(true); view.render(model, request, response); - assertEquals("no-cache", response.getHeader("Pragma")); - assertEquals("no-cache, no-store, max-age=0", response.getHeader("Cache-Control")); - assertNotNull(response.getHeader("Expires")); + assertEquals("no-store", response.getHeader("Cache-Control")); assertEquals(MappingJackson2XmlView.DEFAULT_CONTENT_TYPE, response.getContentType()); @@ -127,9 +125,7 @@ public class MappingJackson2XmlViewTests { view.render(model, request, response); - assertNull(response.getHeader("Pragma")); assertNull(response.getHeader("Cache-Control")); - assertNull(response.getHeader("Expires")); } @Test diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-resources-chain-no-auto.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-resources-chain-no-auto.xml index 907fdc9830..e48d73ac21 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-resources-chain-no-auto.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-resources-chain-no-auto.xml @@ -6,6 +6,7 @@ http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> +