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 aa1598666cb..23726ae4dba 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 df581d5c6f4..c30e2c74f62 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);