Add spring.web.resources.cache.use-last-modified

Prior to this commit, packaging a Spring Boot application as a container
image with Cloud Native Buildpacks could result in unwanted browser
caching behavior, with "Last-Modified" HTTP response headers pointing to
dates in the far past.

This is due to CNB resetting the last-modified date metadata for static
files (for build reproducibility and container layer caching) and Spring
static resource handling relying on that information when serving static
resources.

This commit introduces a new configuration property
`spring.web.resources.cache.use-last-modified` that can be used to
disable this behavior in Spring if the application is meant to run as a
container image built by CNB.

The default value for this property remains `true` since this remains
the default value in Spring Framework and using that information in
other deployment models is a perfectly valid use case.

Fixes gh-24099
This commit is contained in:
Brian Clozel
2020-11-10 13:40:25 +01:00
parent 673a5ac2fd
commit f0a6128db3
6 changed files with 59 additions and 23 deletions

View File

@@ -361,6 +361,12 @@ public class WebProperties {
*/
private final Cachecontrol cachecontrol = new Cachecontrol();
/**
* Whether we should use the "lastModified" metadata of the files in HTTP
* caching headers. Enabled by default.
*/
private boolean useLastModified = true;
public Duration getPeriod() {
return this.period;
}
@@ -374,6 +380,14 @@ public class WebProperties {
return this.cachecontrol;
}
public boolean isUseLastModified() {
return this.useLastModified;
}
public void setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified;
}
private boolean hasBeenCustomized() {
return this.customized || getCachecontrol().hasBeenCustomized();
}

View File

@@ -205,6 +205,7 @@ public class WebFluxAutoConfiguration {
cacheControl.setMaxAge(cachePeriod);
}
registration.setCacheControl(cacheControl.toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
}
@Override

View File

@@ -329,13 +329,15 @@ public class WebMvcAutoConfiguration {
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)
.setUseLastModified(this.resourceProperties.getCache().isUseLastModified()));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)
.setUseLastModified(this.resourceProperties.getCache().isUseLastModified()));
}
}

View File

@@ -449,6 +449,19 @@ class WebFluxAutoConfigurationTests {
Assertions.setExtractBareNamePropertyMethods(true);
}
@Test
void useLastModified() {
this.contextRunner.withPropertyValues("spring.web.resources.cache.use-last-modified=false").run((context) -> {
Map<PathPattern, Object> handlerMap = getHandlerMap(context);
assertThat(handlerMap).hasSize(2);
for (Object handler : handlerMap.values()) {
if (handler instanceof ResourceWebHandler) {
assertThat(((ResourceWebHandler) handler).isUseLastModified()).isFalse();
}
}
});
}
@Test
void customPrinterAndParserShouldBeRegisteredAsConverters() {
this.contextRunner.withUserConfiguration(ParserConfiguration.class, PrinterConfiguration.class)

View File

@@ -28,7 +28,6 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@@ -754,26 +753,25 @@ class WebMvcAutoConfigurationTests {
@ParameterizedTest
@ValueSource(strings = { "spring.resources.", "spring.web.resources." })
void cachePeriod(String prefix) {
this.contextRunner.withPropertyValues(prefix + "cache.period:5").run(this::assertCachePeriod);
}
private void assertCachePeriod(AssertableWebApplicationContext context) {
Map<String, Object> handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class));
assertThat(handlerMap).hasSize(2);
for (Entry<String, Object> entry : handlerMap.entrySet()) {
Object handler = entry.getValue();
if (handler instanceof ResourceHttpRequestHandler) {
assertThat(((ResourceHttpRequestHandler) handler).getCacheSeconds()).isEqualTo(5);
assertThat(((ResourceHttpRequestHandler) handler).getCacheControl()).isNull();
}
}
this.contextRunner.withPropertyValues(prefix + "cache.period:5").run((context) -> {
assertResourceHttpRequestHandler((context), (handler) -> {
assertThat(handler.getCacheSeconds()).isEqualTo(5);
assertThat(handler.getCacheControl()).isNull();
});
});
}
@ParameterizedTest
@ValueSource(strings = { "spring.resources.", "spring.web.resources." })
void cacheControl(String prefix) {
this.contextRunner.withPropertyValues(prefix + "cache.cachecontrol.max-age:5",
prefix + "cache.cachecontrol.proxy-revalidate:true").run(this::assertCacheControl);
this.contextRunner
.withPropertyValues(prefix + "cache.cachecontrol.max-age:5",
prefix + "cache.cachecontrol.proxy-revalidate:true")
.run((context) -> assertResourceHttpRequestHandler(context, (handler) -> {
assertThat(handler.getCacheSeconds()).isEqualTo(-1);
assertThat(handler.getCacheControl()).usingRecursiveComparison()
.isEqualTo(CacheControl.maxAge(5, TimeUnit.SECONDS).proxyRevalidate());
}));
}
@Test
@@ -939,14 +937,20 @@ class WebMvcAutoConfigurationTests {
});
}
private void assertCacheControl(AssertableWebApplicationContext context) {
@Test
void lastModifiedNotUsedIfDisabled() {
this.contextRunner.withPropertyValues("spring.web.resources.cache.use-last-modified=false")
.run((context) -> assertResourceHttpRequestHandler(context,
(handler) -> assertThat(handler.isUseLastModified()).isFalse()));
}
private void assertResourceHttpRequestHandler(AssertableWebApplicationContext context,
Consumer<ResourceHttpRequestHandler> handlerConsumer) {
Map<String, Object> handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class));
assertThat(handlerMap).hasSize(2);
for (Object handler : handlerMap.keySet()) {
if (handler instanceof ResourceHttpRequestHandler) {
assertThat(((ResourceHttpRequestHandler) handler).getCacheSeconds()).isEqualTo(-1);
assertThat(((ResourceHttpRequestHandler) handler).getCacheControl()).usingRecursiveComparison()
.isEqualTo(CacheControl.maxAge(5, TimeUnit.SECONDS).proxyRevalidate());
handlerConsumer.accept((ResourceHttpRequestHandler) handler);
}
}
}