From 7582bd8667a808d8a4302f4e46c98535d6e78edc Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 25 Oct 2023 11:44:31 +0200 Subject: [PATCH] Support ETag generation on ResourceHttpRequestHandler Prior to this commit, the `ResourceHttpRequestHandler` would support HTTP caching when serving resources, but only driving it through the `Resource#lastModified()` information. This commit introduces an ETag generator function that can be configured on the `ResourceHttpRequestHandler` to dynamically generate an ETag value for the Resource that is going to be served. Closes gh-29031 --- .../resource/ResourceHttpRequestHandler.java | 31 ++++++++++++++++- .../ResourceHttpRequestHandlerTests.java | 34 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 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 aa1598666c..23726ae4db 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 @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Function; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -141,6 +142,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator private boolean useLastModified = true; + @Nullable + private Function etagGenerator; + private boolean optimizeLocations = false; @Nullable @@ -383,6 +387,29 @@ public class ResourceHttpRequestHandler extends WebContentGenerator return this.useLastModified; } + /** + * Configure a generator function that will be used to create the ETag information, + * given a {@link Resource} that is about to be written to the response. + *

This function should return a String that will be used as an argument in + * {@link ServletWebRequest#checkNotModified(String)}, or {@code null} if no value + * can be generated for the given resource. + * @param etagGenerator the HTTP ETag generator function to use. + * @since 6.1 + */ + public void setEtagGenerator(@Nullable Function etagGenerator) { + this.etagGenerator = etagGenerator; + } + + /** + * Return the HTTP ETag generator function to be used when serving resources. + * @return the HTTP ETag generator function + * @since 6.1 + */ + @Nullable + public Function getEtagGenerator() { + return this.etagGenerator; + } + /** * Set whether to optimize the specified locations through an existence * check on startup, filtering non-existing directories upfront so that @@ -567,7 +594,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator checkRequest(request); // Header phase - if (isUseLastModified() && new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { + String eTagValue = (this.getEtagGenerator() != null) ? this.getEtagGenerator().apply(resource) : null; + long lastModified = (this.isUseLastModified()) ? resource.lastModified() : -1; + if (new ServletWebRequest(request, response).checkNotModified(eTagValue, lastModified)) { logger.trace("Resource not modified"); return; } 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 df581d5c6f..c30e2c74f6 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 @@ -478,7 +478,7 @@ class ResourceHttpRequestHandlerTests { } @Test - void shouldRespondWithNotModified() throws Exception { + void shouldRespondWithNotModifiedWhenModifiedSince() throws Exception { this.handler.afterPropertiesSet(); this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); this.request.addHeader("If-Modified-Since", resourceLastModified("test/foo.css")); @@ -496,6 +496,38 @@ class ResourceHttpRequestHandlerTests { assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }"); } + @Test + void shouldRespondWithNotModifiedWhenEtag() throws Exception { + this.handler.setEtagGenerator(resource -> "testEtag"); + this.handler.afterPropertiesSet(); + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); + this.request.addHeader("If-None-Match", "\"testEtag\""); + this.handler.handleRequest(this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_MODIFIED); + } + + @Test + void shouldRespondWithModifiedResourceWhenEtagNoMatch() throws Exception { + this.handler.setEtagGenerator(resource -> "noMatch"); + this.handler.afterPropertiesSet(); + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); + this.request.addHeader("If-None-Match", "\"testEtag\""); + this.handler.handleRequest(this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }"); + } + + @Test + void shouldRespondWithNotModifiedWhenEtagAndLastModified() throws Exception { + this.handler.setEtagGenerator(resource -> "testEtag"); + this.handler.afterPropertiesSet(); + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); + this.request.addHeader("If-None-Match", "\"testEtag\""); + this.request.addHeader("If-Modified-Since", resourceLastModified("test/foo.css")); + this.handler.handleRequest(this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_MODIFIED); + } + @Test // SPR-14005 void overwritesExistingCacheControlHeaders() throws Exception { this.handler.setCacheSeconds(3600);