diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java index a4982ac857..5686f6c8b5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java @@ -48,6 +48,8 @@ public class ResourceHandlerRegistration { @Nullable private ResourceChainRegistration resourceChainRegistration; + private boolean useLastModified = true; + /** * Create a {@link ResourceHandlerRegistration} instance. @@ -94,6 +96,18 @@ public class ResourceHandlerRegistration { return this; } + /** + * Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses. + *

This configuration is set to {@code true} by default. + * @param useLastModified whether the "last modified" resource information should be used. + * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation + * @since 5.3 + */ + public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; + return this; + } + /** * Configure a chain of resource resolvers and transformers to use. This * can be useful, for example, to apply a version strategy to resource URLs. @@ -153,6 +167,7 @@ public class ResourceHandlerRegistration { if (this.cacheControl != null) { handler.setCacheControl(this.cacheControl); } + handler.setUseLastModified(this.useLastModified); return handler; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java index 87d59e7cd7..9914780626 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java @@ -114,6 +114,8 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { @Nullable private ResourceLoader resourceLoader; + private boolean useLastModified = true; + /** * Accepts a list of String-based location values to be resolved into @@ -237,6 +239,27 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { this.resourceLoader = resourceLoader; } + /** + * Return whether the {@link Resource#lastModified()} information is used + * to drive HTTP responses when serving static resources. + * @since 5.3 + */ + public boolean isUseLastModified() { + return this.useLastModified; + } + + /** + * Set whether we should look at the {@link Resource#lastModified()} + * when serving resources and use this information to drive {@code "Last-Modified"} + * HTTP response headers. + *

This option is enabled by default and should be turned off if the metadata of + * the static files should be ignored. + * @param useLastModified whether to use the resource last-modified information. + * @since 5.3 + */ + public void setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; + } @Override public void afterPropertiesSet() throws Exception { @@ -339,7 +362,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { } // Header phase - if (exchange.checkNotModified(Instant.ofEpochMilli(resource.lastModified()))) { + if (isUseLastModified() && exchange.checkNotModified(Instant.ofEpochMilli(resource.lastModified()))) { logger.trace(exchange.getLogPrefix() + "Resource not modified"); return Mono.empty(); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java index ac14056642..3401d61972 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java @@ -212,6 +212,12 @@ public class ResourceHandlerRegistryTests { assertThat(transformers.get(2)).isSameAs(cssLinkTransformer); } + @Test + void ignoreLastModified() { + this.registration.setUseLastModified(false); + assertThat(getHandler("/resources/**").isUseLastModified()).isFalse(); + } + private ResourceWebHandler getHandler(String pathPattern) { SimpleUrlHandlerMapping mapping = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java index fc8f292258..31052acbe9 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java @@ -631,6 +631,20 @@ public class ResourceWebHandlerTests { assertThat(exchange.getResponse().getHeaders().getCacheControl()).isEqualTo("max-age=3600"); } + @Test + void ignoreLastModified() { + MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("")); + setPathWithinHandlerMapping(exchange, "foo.css"); + this.handler.setUseLastModified(false); + this.handler.handle(exchange).block(TIMEOUT); + + HttpHeaders headers = exchange.getResponse().getHeaders(); + assertThat(headers.getContentType()).isEqualTo(MediaType.parseMediaType("text/css")); + assertThat(headers.getContentLength()).isEqualTo(17); + assertThat(headers.containsKey("Last-Modified")).isFalse(); + assertResponseBody(exchange, "h1 { color:red; }"); + } + private void setPathWithinHandlerMapping(ServerWebExchange exchange, String path) { exchange.getAttributes().put(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java index 7c716bf4f3..f5c31028b7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.List; import org.springframework.cache.Cache; +import org.springframework.core.io.Resource; import org.springframework.http.CacheControl; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -50,6 +51,8 @@ public class ResourceHandlerRegistration { @Nullable private ResourceChainRegistration resourceChainRegistration; + private boolean useLastModified = true; + /** * Create a {@link ResourceHandlerRegistration} instance. @@ -109,6 +112,18 @@ public class ResourceHandlerRegistration { return this; } + /** + * Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses. + *

This configuration is set to {@code true} by default. + * @param useLastModified whether the "last modified" resource information should be used. + * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation + * @since 5.3 + */ + public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; + return this; + } + /** * Configure a chain of resource resolvers and transformers to use. This * can be useful, for example, to apply a version strategy to resource URLs. @@ -172,6 +187,7 @@ public class ResourceHandlerRegistration { else if (this.cachePeriod != null) { handler.setCacheSeconds(this.cachePeriod); } + handler.setUseLastModified(this.useLastModified); return handler; } 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 186c79d7bb..21c8683d45 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 @@ -140,6 +140,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator @Nullable private StringValueResolver embeddedValueResolver; + private boolean useLastModified = true; + public ResourceHttpRequestHandler() { super(HttpMethod.GET.name(), HttpMethod.HEAD.name()); @@ -346,6 +348,27 @@ public class ResourceHttpRequestHandler extends WebContentGenerator this.embeddedValueResolver = resolver; } + /** + * Return whether the {@link Resource#lastModified()} information is used + * to drive HTTP responses when serving static resources. + * @since 5.3 + */ + public boolean isUseLastModified() { + return this.useLastModified; + } + + /** + * Set whether we should look at the {@link Resource#lastModified()} + * when serving resources and use this information to drive {@code "Last-Modified"} + * HTTP response headers. + *

This option is enabled by default and should be turned off if the metadata of + * the static files should be ignored. + * @param useLastModified whether to use the resource last-modified information. + * @since 5.3 + */ + public void setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; + } @Override public void afterPropertiesSet() throws Exception { @@ -498,7 +521,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator checkRequest(request); // Header phase - if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { + if (isUseLastModified() && new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { logger.trace("Resource not modified"); return; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java index 0f51a1e82e..0551598c49 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -238,6 +238,13 @@ public class ResourceHandlerRegistryTests { assertThat(locationCharsets.values().iterator().next()).isEqualTo(StandardCharsets.ISO_8859_1); } + @Test + void lastModifiedDisabled() { + this.registration.setUseLastModified(false); + ResourceHttpRequestHandler handler = getHandler("/resources/**"); + assertThat(handler.isUseLastModified()).isFalse(); + } + private ResourceHttpRequestHandler getHandler(String pathPattern) { SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping(); return (ResourceHttpRequestHandler) hm.getUrlMap().get(pathPattern); 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 03a5167db9..6a17d8aa41 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 @@ -667,6 +667,18 @@ public class ResourceHttpRequestHandlerTests { assertThat(this.response.getHeader("Cache-Control")).isEqualTo("max-age=3600"); } + @Test + public void ignoreLastModified() throws Exception { + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); + this.handler.setUseLastModified(false); + this.handler.handleRequest(this.request, this.response); + + assertThat(this.response.getContentType()).isEqualTo("text/css"); + assertThat(this.response.getContentLength()).isEqualTo(17); + assertThat(this.response.containsHeader("Last-Modified")).isFalse(); + assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }"); + } + private long resourceLastModified(String resourceName) throws IOException { return new ClassPathResource(resourceName, getClass()).getFile().lastModified(); diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index 250626cb82..b91245cd1f 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -5621,8 +5621,8 @@ In the next example, given a request that starts with `/resources`, the relative used to find and serve static resources relative to `/public` under the web application root or on the classpath under `/static`. The resources are served with a one-year future expiration to ensure maximum use of the browser cache and a reduction in HTTP requests -made by the browser. The `Last-Modified` header is also evaluated and, if present, a `304` -status code is returned. +made by the browser. The `Last-Modified` information is deduced from `Resource#lastModified` +so that HTTP conditional requests are supported with `"Last-Modified"` headers. The following listing shows how to do so with Java configuration: @@ -5637,7 +5637,7 @@ The following listing shows how to do so with Java configuration: public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**") .addResourceLocations("/public", "classpath:/static/") - .setCachePeriod(31556926); + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); } } ---- @@ -5651,7 +5651,7 @@ The following listing shows how to do so with Java configuration: override fun addResourceHandlers(registry: ResourceHandlerRegistry) { registry.addResourceHandler("/resources/**") .addResourceLocations("/public", "classpath:/static/") - .setCachePeriod(31556926) + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))) } } ----