Skip to content

Commit 7582bd8

Browse files
committed
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
1 parent ebfa009 commit 7582bd8

File tree

2 files changed

+63
-2
lines changed

2 files changed

+63
-2
lines changed

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.List;
2626
import java.util.Locale;
2727
import java.util.Map;
28+
import java.util.function.Function;
2829

2930
import jakarta.servlet.ServletException;
3031
import jakarta.servlet.http.HttpServletRequest;
@@ -141,6 +142,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
141142

142143
private boolean useLastModified = true;
143144

145+
@Nullable
146+
private Function<Resource, String> etagGenerator;
147+
144148
private boolean optimizeLocations = false;
145149

146150
@Nullable
@@ -383,6 +387,29 @@ public boolean isUseLastModified() {
383387
return this.useLastModified;
384388
}
385389

390+
/**
391+
* Configure a generator function that will be used to create the ETag information,
392+
* given a {@link Resource} that is about to be written to the response.
393+
* <p>This function should return a String that will be used as an argument in
394+
* {@link ServletWebRequest#checkNotModified(String)}, or {@code null} if no value
395+
* can be generated for the given resource.
396+
* @param etagGenerator the HTTP ETag generator function to use.
397+
* @since 6.1
398+
*/
399+
public void setEtagGenerator(@Nullable Function<Resource, String> etagGenerator) {
400+
this.etagGenerator = etagGenerator;
401+
}
402+
403+
/**
404+
* Return the HTTP ETag generator function to be used when serving resources.
405+
* @return the HTTP ETag generator function
406+
* @since 6.1
407+
*/
408+
@Nullable
409+
public Function<Resource, String> getEtagGenerator() {
410+
return this.etagGenerator;
411+
}
412+
386413
/**
387414
* Set whether to optimize the specified locations through an existence
388415
* check on startup, filtering non-existing directories upfront so that
@@ -567,7 +594,9 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon
567594
checkRequest(request);
568595

569596
// Header phase
570-
if (isUseLastModified() && new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
597+
String eTagValue = (this.getEtagGenerator() != null) ? this.getEtagGenerator().apply(resource) : null;
598+
long lastModified = (this.isUseLastModified()) ? resource.lastModified() : -1;
599+
if (new ServletWebRequest(request, response).checkNotModified(eTagValue, lastModified)) {
571600
logger.trace("Resource not modified");
572601
return;
573602
}

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ void configureVersionResourceResolver() throws Exception {
478478
}
479479

480480
@Test
481-
void shouldRespondWithNotModified() throws Exception {
481+
void shouldRespondWithNotModifiedWhenModifiedSince() throws Exception {
482482
this.handler.afterPropertiesSet();
483483
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
484484
this.request.addHeader("If-Modified-Since", resourceLastModified("test/foo.css"));
@@ -496,6 +496,38 @@ void shouldRespondWithModifiedResource() throws Exception {
496496
assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }");
497497
}
498498

499+
@Test
500+
void shouldRespondWithNotModifiedWhenEtag() throws Exception {
501+
this.handler.setEtagGenerator(resource -> "testEtag");
502+
this.handler.afterPropertiesSet();
503+
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
504+
this.request.addHeader("If-None-Match", "\"testEtag\"");
505+
this.handler.handleRequest(this.request, this.response);
506+
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_MODIFIED);
507+
}
508+
509+
@Test
510+
void shouldRespondWithModifiedResourceWhenEtagNoMatch() throws Exception {
511+
this.handler.setEtagGenerator(resource -> "noMatch");
512+
this.handler.afterPropertiesSet();
513+
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
514+
this.request.addHeader("If-None-Match", "\"testEtag\"");
515+
this.handler.handleRequest(this.request, this.response);
516+
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
517+
assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }");
518+
}
519+
520+
@Test
521+
void shouldRespondWithNotModifiedWhenEtagAndLastModified() throws Exception {
522+
this.handler.setEtagGenerator(resource -> "testEtag");
523+
this.handler.afterPropertiesSet();
524+
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
525+
this.request.addHeader("If-None-Match", "\"testEtag\"");
526+
this.request.addHeader("If-Modified-Since", resourceLastModified("test/foo.css"));
527+
this.handler.handleRequest(this.request, this.response);
528+
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_MODIFIED);
529+
}
530+
499531
@Test // SPR-14005
500532
void overwritesExistingCacheControlHeaders() throws Exception {
501533
this.handler.setCacheSeconds(3600);

0 commit comments

Comments
 (0)