提交 38f32e38 编写于 作者: B Brian Clozel

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

```
<mvc:resources mapping="/resources/**" location="classpath:/resources/">
  <mvc:cachecontrol max-age="3600" cache-public="true"/>
</mvc:resources>
```

Issue: SPR-2779, SPR-6834, SPR-7129, SPR-9543, SPR-10464
上级 953608ec
/*
* 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.
*
* <p>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.
*
* <ul>
* <li>Caching HTTP responses with {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS)}
* will result in {@code Cache-Control: "max-age=3600"}</li>
* <li>Preventing cache with {@code CacheControl cc = CacheControl.noStore()}
* will result in {@code Cache-Control: "no-store"}</li>
* <li>Advanced cases like {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).noTransform().cachePublic()}
* will result in {@code Cache-Control: "max-age=3600, no-transform, public"}</li>
* </ul>
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2">rfc7234 section 5.2.2</a>
* @see <a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching">
* HTTP caching - Google developers reference</a>
* @see <a href="https://www.mnot.net/cache_docs/">Mark Nottingham's cache documentation</a>
* @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.
*
* <p>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.
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.8">rfc7234 section 5.2.2.8</a>
*/
public static CacheControl maxAge(long maxAge, TimeUnit unit) {
CacheControl cc = new CacheControl();
cc.maxAge = unit.toSeconds(maxAge);
return cc;
}
/**
* Add a "no-store" directive
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.3">rfc7234 section 5.2.2.3</a>
*/
public static CacheControl noStore() {
CacheControl cc = new CacheControl();
cc.noStore = true;
return cc;
}
/**
* Add a "no-cache" directive.
*
* <p>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.
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.2">rfc7234 section 5.2.2.2</a>
*/
public static CacheControl noCache() {
CacheControl cc = new CacheControl();
cc.noCache = true;
return cc;
}
/**
* Return an empty directive.
*
* <p>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
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.1">rfc7234 section 5.2.2.1</a>
*/
public CacheControl mustRevalidate() {
this.mustRevalidate = true;
return this;
}
/**
* Add a "no-transform" directive
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.4">rfc7234 section 5.2.2.4</a>
*/
public CacheControl noTransform() {
this.noTransform = true;
return this;
}
/**
* Add a "public" directive
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.5">rfc7234 section 5.2.2.5</a>
*/
public CacheControl cachePublic() {
this.cachePublic = true;
return this;
}
/**
* Add a "private" directive
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.6">rfc7234 section 5.2.2.6</a>
*/
public CacheControl cachePrivate() {
this.cachePrivate = true;
return this;
}
/**
* Add a "proxy-revalidate" directive
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.7">rfc7234 section 5.2.2.7</a>
*/
public CacheControl proxyRevalidate() {
this.proxyRevalidate = true;
return this;
}
/**
* Add a "s-maxage" directive
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.9">rfc7234 section 5.2.2.9</a>
*/
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);
}
}
/*
* 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"));
}
}
/*
* 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<? super Object> resourceResolvers,
ManagedList<? super Object> resourceTransformers, Element element, Object source) {
......
/*
* 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.
*
* <p>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;
......
/*
* 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");
}
......
/*
* 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) {
......
/*
* 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.
*
* <p>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<String, Integer> cacheMappings = new HashMap<String, Integer>();
private PathMatcher pathMatcher = new AntPathMatcher();
private Map<String, CacheControl> cacheMappings = new HashMap<String, CacheControl>();
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}.
* <p>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.
*
* <p>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.
* <p>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;
}
......
/*
* 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.
......
......@@ -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.
......
......@@ -60,8 +60,8 @@ import org.springframework.web.servlet.support.WebContentGenerator;
* <p>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);
......
/*
* 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}.
*
* <p>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.
*
* <p>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<String> supportedMethods;
private Set<String> 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".
* <p>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".
* <p>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.
* <p>Only if this is set to 0 (no cache) or a positive value (cache for
* this many seconds) will this class generate cache headers.
* <p>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:
* <ul>
* <li>seconds == -1 (default value): no generation cache-related headers</li>
* <li>seconds == 0: "Cache-Control: no-store" will prevent caching</li>
* <li>seconds > 0: "Cache-Control: max-age=seconds" will ask to cache content</li>
* </ul>
* <p>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,41 +358,24 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
}
}
// Do declarative cache control.
// Revalidate if the controller supports last-modified.
applyCacheSeconds(response, cacheSeconds, lastModified);
}
/**
* Prevent the response from being cached.
* See {@code http://www.mnot.net/cache_docs}.
*/
protected final void preventCaching(HttpServletResponse response) {
response.setHeader(HEADER_PRAGMA, "no-cache");
if (this.useExpiresHeader) {
// HTTP 1.0 header
response.setDateHeader(HEADER_EXPIRES, 1L);
if (this.usePreviousHttpCachingBehavior) {
addHttp10CacheHeaders(response);
}
if (this.useCacheControlHeader) {
// HTTP 1.1 header: "no-cache" is the standard value,
// "no-store" is necessary to prevent caching on FireFox.
response.setHeader(HEADER_CACHE_CONTROL, "no-cache");
if (this.useCacheControlNoStore) {
response.addHeader(HEADER_CACHE_CONTROL, "no-store");
else if (cacheControl != null) {
String ccValue = cacheControl.getHeaderValue();
if (ccValue != null) {
response.setHeader(HEADER_CACHE_CONTROL, ccValue);
}
}
}
/**
* 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);
protected void addHttp10CacheHeaders(HttpServletResponse response) {
if (this.cacheSeconds > 0) {
cacheForSeconds(response, this.cacheSeconds, this.alwaysMustRevalidate);
}
else if (this.cacheSeconds == 0) {
preventCaching(response);
}
}
/**
......@@ -335,7 +396,7 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
if (this.useCacheControlHeader) {
// HTTP 1.1 header
String headerValue = "max-age=" + seconds;
if (mustRevalidate || this.alwaysMustRevalidate) {
if (mustRevalidate) {
headerValue += ", must-revalidate";
}
response.setHeader(HEADER_CACHE_CONTROL, headerValue);
......@@ -343,39 +404,23 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
}
/**
* 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.
* <p>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)
* Prevent the response from being cached.
* See {@code http://www.mnot.net/cache_docs}.
*/
protected final void applyCacheSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
if (seconds > 0) {
cacheForSeconds(response, seconds, mustRevalidate);
protected final void preventCaching(HttpServletResponse response) {
response.setHeader(HEADER_PRAGMA, "no-cache");
if (this.useExpiresHeader) {
// HTTP 1.0 header
response.setDateHeader(HEADER_EXPIRES, 1L);
}
else if (seconds == 0) {
preventCaching(response);
if (this.useCacheControlHeader) {
// HTTP 1.1 header: "no-cache" is the standard value,
// "no-store" is necessary to prevent caching on FireFox.
response.setHeader(HEADER_CACHE_CONTROL, "no-cache");
if (this.useCacheControlNoStore) {
response.addHeader(HEADER_CACHE_CONTROL, "no-store");
}
}
// Leave caching to the client otherwise.
}
}
/*
* 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");
}
}
......
......@@ -477,6 +477,86 @@
<xsd:attribute name="cache-name" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="cachecontrol">
<xsd:annotation>
<xsd:documentation source="org.springframework.web.cache.CacheControl"><![CDATA[
Generates "Cache-Control" HTTP response headers.
]]></xsd:documentation>
</xsd:annotation>
<xsd:attribute name="must-revalidate" type="xsd:boolean" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
Adds a "must-revalidate" directive in the Cache-Control header.
This indicates that caches should revalidate the cached response when it's become stale.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="no-cache" type="xsd:boolean" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
Adds a "no-cache" directive in the Cache-Control header.
This indicates that caches should always revalidate cached response with the server.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="no-store" type="xsd:boolean" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
Adds a "no-store" directive in the Cache-Control header.
This indicates that caches should never cache the response.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="no-transform" type="xsd:boolean" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
Adds a "no-transform" directive in the Cache-Control header.
This indicates that caches should never transform (i.e. compress, optimize) the response content.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="cache-public" type="xsd:boolean" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
Adds a "public" directive in the Cache-Control header.
This indicates that any cache MAY store the response.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="cache-private" type="xsd:boolean" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
Adds a "private" directive in the Cache-Control header.
This indicates that the response is intended for a single user and may not be stored by shared caches.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="proxy-revalidate" type="xsd:boolean" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
Adds a "proxy-revalidate" directive in the Cache-Control header.
This directive has the same meaning as the "must-revalidate" directive, except it only applies to shared caches.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="max-age" type="xsd:int" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
Adds a "max-age" directive in the Cache-Control header.
This indicates that the response should be cached for the given number of seconds.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="s-maxage" type="xsd:int" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
Adds a "s-maxage" directive in the Cache-Control header.
This directive has the same meaning as the "max-age" directive, except it only applies to shared caches.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:element name="resources">
<xsd:annotation>
<xsd:documentation
......@@ -487,6 +567,7 @@
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="cachecontrol" type="cachecontrol" minOccurs="0" maxOccurs="1"/>
<xsd:element name="resource-chain" type="resource-chain" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="mapping" use="required" type="xsd:string">
......
......@@ -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<ResourceResolver> 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;
}
}
......
/*
* 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
......
/*
* 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);
......
/*
* 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
......
......@@ -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());
......
/*
* 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
......
/*
* 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
......
......@@ -6,6 +6,7 @@
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/">
<mvc:cachecontrol max-age="3600" s-maxage="1800" cache-public="true"/>
<mvc:resource-chain resource-cache="false" auto-registration="false">
<mvc:resolvers>
<mvc:version-resolver>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册