Browse Source

Set ETag header with VersionResourceResolver

Prior to this change, VersionResourceResolver and VersionStrategy would
resolve static resources using version strings. They assist
ResourceHttpRequestHandler with serving static resources. The
RequestHandler itself can be configured with HTTP caching strategies to
set Cache-Control headers.

In order to have a complete strategy with Cache-Control and ETag
response headers, developers can't reuse that version string information
and have to rely on other mechanisms (like ShallowEtagHeaderFilter).

This commit makes VersionResourceResolver use that version string to set
it as a request attribute, which will be used by the
ResourceHttpRequestHandler to write an ETag response header.

Issue: SPR-13382
pull/864/merge
Brian Clozel 9 years ago
parent
commit
190eb6ace1
  1. 21
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
  2. 7
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java
  3. 2
      spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java
  4. 11
      spring-webmvc/src/test/java/org/springframework/web/servlet/resource/VersionResourceResolverTests.java
  5. 5
      src/asciidoc/web-mvc.adoc

21
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java

@ -89,13 +89,12 @@ import org.springframework.web.servlet.support.WebContentGenerator; @@ -89,13 +89,12 @@ import org.springframework.web.servlet.support.WebContentGenerator;
* @author Jeremy Grelle
* @author Juergen Hoeller
* @author Arjen Poutsma
* @author Brian Clozel
* @since 3.0.4
*/
public class ResourceHttpRequestHandler extends WebContentGenerator
implements HttpRequestHandler, InitializingBean, CorsConfigurationSource {
private static final String CONTENT_ENCODING = "Content-Encoding";
private static final Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class);
private static final boolean jafPresent = ClassUtils.isPresent(
@ -267,6 +266,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator @@ -267,6 +266,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
}
if (request.getHeader(HttpHeaders.RANGE) == null) {
setETagHeader(request, response);
setHeaders(response, resource, mediaType);
writeContent(response, resource);
}
@ -406,6 +406,21 @@ public class ResourceHttpRequestHandler extends WebContentGenerator @@ -406,6 +406,21 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return mediaType;
}
/**
* Set the ETag header if the version string of the served resource is present.
* Version strings can be resolved by {@link VersionStrategy} implementations and then
* set as a request attribute by {@link VersionResourceResolver}.
* @param request current servlet request
* @param response current servlet response
* @see VersionResourceResolver
*/
protected void setETagHeader(HttpServletRequest request, HttpServletResponse response) {
String versionString = (String) request.getAttribute(VersionResourceResolver.RESOURCE_VERSION_ATTRIBUTE);
if(versionString != null) {
response.setHeader(HttpHeaders.ETAG, "\"" + versionString + "\"");
}
}
/**
* Set headers on the given servlet response.
* Called for GET requests as well as HEAD requests.
@ -426,7 +441,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator @@ -426,7 +441,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
}
if (resource instanceof EncodedResource) {
response.setHeader(CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding());
response.setHeader(HttpHeaders.CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding());
}
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");

7
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java

@ -1,5 +1,5 @@ @@ -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.
@ -54,6 +54,8 @@ import org.springframework.util.StringUtils; @@ -54,6 +54,8 @@ import org.springframework.util.StringUtils;
*/
public class VersionResourceResolver extends AbstractResourceResolver {
public static final String RESOURCE_VERSION_ATTRIBUTE = VersionResourceResolver.class.getName() + ".resourceVersion";
private AntPathMatcher pathMatcher = new AntPathMatcher();
/** Map from path pattern -> VersionStrategy */
@ -165,6 +167,9 @@ public class VersionResourceResolver extends AbstractResourceResolver { @@ -165,6 +167,9 @@ public class VersionResourceResolver extends AbstractResourceResolver {
if (logger.isTraceEnabled()) {
logger.trace("resource matches extracted version");
}
if(request != null) {
request.setAttribute(VersionResourceResolver.RESOURCE_VERSION_ATTRIBUTE, candidateVersion);
}
return baseResource;
}
else {

2
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java

@ -87,6 +87,7 @@ public class ResourceHttpRequestHandlerTests { @@ -87,6 +87,7 @@ public class ResourceHttpRequestHandlerTests {
@Test
public void getResource() throws Exception {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
this.request.setAttribute(VersionResourceResolver.RESOURCE_VERSION_ATTRIBUTE, "versionString");
this.handler.handleRequest(this.request, this.response);
assertEquals("text/css", this.response.getContentType());
@ -94,6 +95,7 @@ public class ResourceHttpRequestHandlerTests { @@ -94,6 +95,7 @@ public class ResourceHttpRequestHandlerTests {
assertEquals("max-age=3600", this.response.getHeader("Cache-Control"));
assertTrue(this.response.containsHeader("Last-Modified"));
assertEquals(this.response.getHeader("Last-Modified"), resourceLastModifiedDate("test/foo.css"));
assertEquals("\"versionString\"", this.response.getHeader("ETag"));
assertEquals("h1 { color:red; }", this.response.getContentAsString());
}

11
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/VersionResourceResolverTests.java

@ -1,5 +1,5 @@ @@ -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.
@ -26,6 +26,7 @@ import org.junit.Test; @@ -26,6 +26,7 @@ import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.mock.web.test.MockHttpServletRequest;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
@ -135,17 +136,19 @@ public class VersionResourceResolverTests { @@ -135,17 +136,19 @@ public class VersionResourceResolverTests {
String version = "version";
String file = "bar.css";
Resource expected = new ClassPathResource("test/" + file, getClass());
given(this.chain.resolveResource(null, versionFile, this.locations)).willReturn(null);
given(this.chain.resolveResource(null, file, this.locations)).willReturn(expected);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/resources/bar-version.css");
given(this.chain.resolveResource(request, versionFile, this.locations)).willReturn(null);
given(this.chain.resolveResource(request, file, this.locations)).willReturn(expected);
given(this.versionStrategy.extractVersion(versionFile)).willReturn(version);
given(this.versionStrategy.removeVersion(versionFile, version)).willReturn(file);
given(this.versionStrategy.getResourceVersion(expected)).willReturn(version);
this.resolver
.setStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
Resource actual = this.resolver.resolveResourceInternal(null, versionFile, this.locations, this.chain);
Resource actual = this.resolver.resolveResourceInternal(request, versionFile, this.locations, this.chain);
assertEquals(expected, actual);
verify(this.versionStrategy, times(1)).getResourceVersion(expected);
assertEquals(version, request.getAttribute(VersionResourceResolver.RESOURCE_VERSION_ATTRIBUTE));
}
@Test

5
src/asciidoc/web-mvc.adoc

@ -4133,6 +4133,7 @@ responsible for this, along with conditional headers such as `'Last-Modified'` a @@ -4133,6 +4133,7 @@ responsible for this, along with conditional headers such as `'Last-Modified'` a
The `'Cache-Control'` HTTP response header advises private caches (e.g. browsers) and
public caches (e.g. proxies) on how they can cache HTTP responses for further reuse.
mvc-config-static-resources
An http://en.wikipedia.org/wiki/HTTP_ETag[ETag] (entity tag) is an HTTP response header
returned by an HTTP/1.1 compliant web server used to determine change in content at a
given URL. It can be considered to be the more sophisticated successor to the
@ -4310,6 +4311,7 @@ bandwidth, as the rendered response is not sent back over the wire. @@ -4310,6 +4311,7 @@ bandwidth, as the rendered response is not sent back over the wire.
Note that this strategy saves network bandwidth but not CPU, as the full response must be
computed for each request. Other strategies at the controller level (described above) can
save network bandwidth and avoid computation.
mvc-config-static-resources
You configure the `ShallowEtagHeaderFilter` in `web.xml`:
@ -5104,7 +5106,8 @@ can provide arbitrary resolution and transformation of resources. @@ -5104,7 +5106,8 @@ can provide arbitrary resolution and transformation of resources.
The built-in `VersionResourceResolver` can be configured with different strategies.
For example a `FixedVersionStrategy` can use a property, a date, or other as the version.
A `ContentVersionStrategy` uses an MD5 hash computed from the content of the resource
(known as "fingerprinting" URLs).
(known as "fingerprinting" URLs). Note that the `VersionResourceResolver` will automatically
use the resolved version strings as HTTP ETag header values when serving resources.
`ContentVersionStrategy` is a good default choice to use except in cases where
it cannot be used (e.g. with JavaScript module loaders). You can configure

Loading…
Cancel
Save