Browse Source

Add option for ignoring last-modified for static resources

Prior to this commit, the resource handler serving static resources for
Spring MVC and Spring WebFlux would always look at the
`Resource#lastModified` information, derive the `"Last-Modified"` HTTP
response header and support HTTP conditional requests with that
information.

In some cases, builds or packaging tools choose to set this last
modification date to a static date in the past. This allows tools to
have reproducible builds or to leverage caching given the static
resources content didn't change.

This can lead to problems where this static date (e.g. "1980-01-01") is
used literally in HTTP responses and will make the HTTP caching
mechanism counter-productive: the content of the resources changed, but
the application insists on saying it didn't change since the 80s...

This commit adds a new configuration option to disable this support -
there is no way to automatically discard those dates: there is no
standard for that and many don't use he "EPOCH 0 date" as it can lead to
compatibility issues with different OSes.

Closes gh-25845
pull/25876/head
Brian Clozel 5 years ago
parent
commit
a0af552d0f
  1. 15
      spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java
  2. 25
      spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java
  3. 6
      spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java
  4. 14
      spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java
  5. 18
      spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java
  6. 25
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
  7. 9
      spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java
  8. 12
      spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java
  9. 8
      src/docs/asciidoc/web/webmvc.adoc

15
spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java

@ -48,6 +48,8 @@ public class ResourceHandlerRegistration {
@Nullable @Nullable
private ResourceChainRegistration resourceChainRegistration; private ResourceChainRegistration resourceChainRegistration;
private boolean useLastModified = true;
/** /**
* Create a {@link ResourceHandlerRegistration} instance. * Create a {@link ResourceHandlerRegistration} instance.
@ -94,6 +96,18 @@ public class ResourceHandlerRegistration {
return this; return this;
} }
/**
* Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses.
* <p>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 * Configure a chain of resource resolvers and transformers to use. This
* can be useful, for example, to apply a version strategy to resource URLs. * can be useful, for example, to apply a version strategy to resource URLs.
@ -153,6 +167,7 @@ public class ResourceHandlerRegistration {
if (this.cacheControl != null) { if (this.cacheControl != null) {
handler.setCacheControl(this.cacheControl); handler.setCacheControl(this.cacheControl);
} }
handler.setUseLastModified(this.useLastModified);
return handler; return handler;
} }

25
spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java

@ -114,6 +114,8 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
@Nullable @Nullable
private ResourceLoader resourceLoader; private ResourceLoader resourceLoader;
private boolean useLastModified = true;
/** /**
* Accepts a list of String-based location values to be resolved into * 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; 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.
* <p>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 @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
@ -339,7 +362,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
} }
// Header phase // Header phase
if (exchange.checkNotModified(Instant.ofEpochMilli(resource.lastModified()))) { if (isUseLastModified() && exchange.checkNotModified(Instant.ofEpochMilli(resource.lastModified()))) {
logger.trace(exchange.getLogPrefix() + "Resource not modified"); logger.trace(exchange.getLogPrefix() + "Resource not modified");
return Mono.empty(); return Mono.empty();
} }

6
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); assertThat(transformers.get(2)).isSameAs(cssLinkTransformer);
} }
@Test
void ignoreLastModified() {
this.registration.setUseLastModified(false);
assertThat(getHandler("/resources/**").isUseLastModified()).isFalse();
}
private ResourceWebHandler getHandler(String pathPattern) { private ResourceWebHandler getHandler(String pathPattern) {
SimpleUrlHandlerMapping mapping = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping(); SimpleUrlHandlerMapping mapping = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping();

14
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"); 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) { private void setPathWithinHandlerMapping(ServerWebExchange exchange, String path) {
exchange.getAttributes().put(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, exchange.getAttributes().put(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE,

18
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 java.util.List;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -50,6 +51,8 @@ public class ResourceHandlerRegistration {
@Nullable @Nullable
private ResourceChainRegistration resourceChainRegistration; private ResourceChainRegistration resourceChainRegistration;
private boolean useLastModified = true;
/** /**
* Create a {@link ResourceHandlerRegistration} instance. * Create a {@link ResourceHandlerRegistration} instance.
@ -109,6 +112,18 @@ public class ResourceHandlerRegistration {
return this; return this;
} }
/**
* Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses.
* <p>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 * Configure a chain of resource resolvers and transformers to use. This
* can be useful, for example, to apply a version strategy to resource URLs. * 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) { else if (this.cachePeriod != null) {
handler.setCacheSeconds(this.cachePeriod); handler.setCacheSeconds(this.cachePeriod);
} }
handler.setUseLastModified(this.useLastModified);
return handler; return handler;
} }

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

@ -140,6 +140,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
@Nullable @Nullable
private StringValueResolver embeddedValueResolver; private StringValueResolver embeddedValueResolver;
private boolean useLastModified = true;
public ResourceHttpRequestHandler() { public ResourceHttpRequestHandler() {
super(HttpMethod.GET.name(), HttpMethod.HEAD.name()); super(HttpMethod.GET.name(), HttpMethod.HEAD.name());
@ -346,6 +348,27 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
this.embeddedValueResolver = resolver; 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.
* <p>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 @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
@ -498,7 +521,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
checkRequest(request); checkRequest(request);
// Header phase // Header phase
if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { if (isUseLastModified() && new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
logger.trace("Resource not modified"); logger.trace("Resource not modified");
return; return;
} }

9
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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); 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) { private ResourceHttpRequestHandler getHandler(String pathPattern) {
SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping(); SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping();
return (ResourceHttpRequestHandler) hm.getUrlMap().get(pathPattern); return (ResourceHttpRequestHandler) hm.getUrlMap().get(pathPattern);

12
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"); 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 { private long resourceLastModified(String resourceName) throws IOException {
return new ClassPathResource(resourceName, getClass()).getFile().lastModified(); return new ClassPathResource(resourceName, getClass()).getFile().lastModified();

8
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 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 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 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` made by the browser. The `Last-Modified` information is deduced from `Resource#lastModified`
status code is returned. so that HTTP conditional requests are supported with `"Last-Modified"` headers.
The following listing shows how to do so with Java configuration: 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) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**") registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/") .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) { override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**") registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/") .addResourceLocations("/public", "classpath:/static/")
.setCachePeriod(31556926) .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)))
} }
} }
---- ----

Loading…
Cancel
Save