From 190eb6ace1acc03e2805dd25f16bc2359195821e Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 25 Aug 2015 16:49:10 +0200 Subject: [PATCH] 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 --- .../resource/ResourceHttpRequestHandler.java | 21 ++++++++++++++++--- .../resource/VersionResourceResolver.java | 7 ++++++- .../ResourceHttpRequestHandlerTests.java | 2 ++ .../VersionResourceResolverTests.java | 11 ++++++---- src/asciidoc/web-mvc.adoc | 5 ++++- 5 files changed, 37 insertions(+), 9 deletions(-) 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 0fe1421148..b636dd133f 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 @@ -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 } 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 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 } 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"); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java index 4e342e58eb..587e795b0f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.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. @@ -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 { if (logger.isTraceEnabled()) { logger.trace("resource matches extracted version"); } + if(request != null) { + request.setAttribute(VersionResourceResolver.RESOURCE_VERSION_ATTRIBUTE, candidateVersion); + } return baseResource; } else { 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 7e20319b5d..176ac6bc04 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 @@ -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 { 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()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/VersionResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/VersionResourceResolverTests.java index c6808f320b..c3fd659dbf 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/VersionResourceResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/VersionResourceResolverTests.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. @@ -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 { 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 diff --git a/src/asciidoc/web-mvc.adoc b/src/asciidoc/web-mvc.adoc index 189fe764ca..f781d38da5 100644 --- a/src/asciidoc/web-mvc.adoc +++ b/src/asciidoc/web-mvc.adoc @@ -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. 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. 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