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:
parent
673a5ac2fd
commit
f0a6128db3
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@ public class WebFluxAutoConfiguration {
|
|||
cacheControl.setMaxAge(cachePeriod);
|
||||
}
|
||||
registration.setCacheControl(cacheControl.toHttpCacheControl());
|
||||
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8925,7 +8925,9 @@ This means you can just type a single command and quickly get a sensible image i
|
|||
|
||||
Refer to the individual plugin documentation on how to use buildpacks with {spring-boot-maven-plugin-docs}#build-image[Maven] and {spring-boot-gradle-plugin-docs}#build-image[Gradle].
|
||||
|
||||
|
||||
NOTE: In order to achieve reproducible builds and container image caching, Buildpacks can manipulate the application resources metadata (such as the file "last modified" information).
|
||||
You should ensure that your application does not rely on that metadata at runtime.
|
||||
Spring Boot can use that information when serving static resources, but this can be disabled with configprop:spring.web.resources.cache.use-last-modified[]
|
||||
|
||||
[[boot-features-whats-next]]
|
||||
== What to Read Next
|
||||
|
|
|
|||
Loading…
Reference in New Issue