From 2a268701c41aaa1324a6c2fcf0e7e71c40f91ec2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 10 Nov 2021 14:21:48 +0100 Subject: [PATCH] Introduce optimizeLocations flag for resource location filtering on startup This flag is off by default since it requires jar files with directory entries. Closes gh-27624 --- .../config/ResourceHandlerRegistration.java | 25 +++- .../reactive/resource/ResourceWebHandler.java | 122 +++++++++++------- .../resource/ResourceWebHandlerTests.java | 4 +- .../ResourceHandlerRegistration.java | 23 +++- .../resource/ResourceHttpRequestHandler.java | 77 +++++++---- .../ResourceHttpRequestHandlerTests.java | 3 +- src/docs/asciidoc/web/webflux.adoc | 7 +- src/docs/asciidoc/web/webmvc.adoc | 7 +- 8 files changed, 188 insertions(+), 80 deletions(-) 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 8ee0e87eafb1..9734347cc665 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 @@ -54,6 +54,8 @@ public class ResourceHandlerRegistration { private boolean useLastModified = true; + private boolean optimizeLocations = false; + @Nullable private Map mediaTypes; @@ -105,15 +107,33 @@ public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) { /** * 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. + * @param useLastModified whether the "last modified" resource information should be used * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation * @since 5.3 + * @see ResourceWebHandler#setUseLastModified */ public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) { this.useLastModified = useLastModified; return this; } + /** + * Set whether to optimize the specified locations through an existence check on startup, + * filtering non-existing directories upfront so that they do not have to be checked + * on every resource access. + *

The default is {@code false}, for defensiveness against zip files without directory + * entries which are unable to expose the existence of a directory upfront. Switch this flag to + * {@code true} for optimized access in case of a consistent jar layout with directory entries. + * @param optimizeLocations whether to optimize the locations through an existence check on startup + * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation + * @since 5.3.13 + * @see ResourceWebHandler#setOptimizeLocations + */ + public ResourceHandlerRegistration setOptimizeLocations(boolean optimizeLocations) { + this.optimizeLocations = optimizeLocations; + 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. @@ -181,8 +201,8 @@ protected String[] getPathPatterns() { */ protected ResourceWebHandler getRequestHandler() { ResourceWebHandler handler = new ResourceWebHandler(); - handler.setLocationValues(this.locationValues); handler.setResourceLoader(this.resourceLoader); + handler.setLocationValues(this.locationValues); if (this.resourceChainRegistration != null) { handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers()); handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers()); @@ -191,6 +211,7 @@ protected ResourceWebHandler getRequestHandler() { handler.setCacheControl(this.cacheControl); } handler.setUseLastModified(this.useLastModified); + handler.setOptimizeLocations(this.optimizeLocations); if (this.mediaTypes != null) { handler.setMediaTypes(this.mediaTypes); } 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 c2ba1b234bf1..e6f0640a7022 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 @@ -72,8 +72,8 @@ *

This request handler may also be configured with a * {@link #setResourceResolvers(List) resourcesResolver} and * {@link #setResourceTransformers(List) resourceTransformer} chains to support - * arbitrary resolution and transformation of resources being served. By default a - * {@link PathResourceResolver} simply finds resources based on the configured + * arbitrary resolution and transformation of resources being served. By default + * a {@link PathResourceResolver} simply finds resources based on the configured * "locations". An application can configure additional resolvers and * transformers such as the {@link VersionResourceResolver} which can resolve * and prepare URLs for resources with a version in the URL. @@ -85,6 +85,7 @@ * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Juergen Hoeller * @since 5.0 */ public class ResourceWebHandler implements WebHandler, InitializingBean { @@ -94,6 +95,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { private static final Log logger = LogFactory.getLog(ResourceWebHandler.class); + @Nullable + private ResourceLoader resourceLoader; + private final List locationValues = new ArrayList<>(4); private final List locationResources = new ArrayList<>(4); @@ -119,11 +123,18 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { @Nullable private Map mediaTypes; - @Nullable - private ResourceLoader resourceLoader; - private boolean useLastModified = true; + private boolean optimizeLocations = false; + + + /** + * Provide the ResourceLoader to load {@link #setLocationValues location values} with. + * @since 5.1 + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } /** * Accepts a list of String-based location values to be resolved into @@ -161,9 +172,9 @@ public void setLocations(@Nullable List locations) { *

Note that if {@link #setLocationValues(List) locationValues} are provided, * instead of loaded Resource-based locations, this method will return * empty until after initialization via {@link #afterPropertiesSet()}. - *

Note: As of 5.3.11 the list of locations is filtered - * to exclude those that don't actually exist and therefore the list returned - * from this method may be a subset of all given locations. + *

Note: As of 5.3.11 the list of locations may be filtered to + * exclude those that don't actually exist and therefore the list returned from this + * method may be a subset of all given locations. See {@link #setOptimizeLocations}. * @see #setLocationValues * @see #setLocations */ @@ -212,6 +223,22 @@ public List getResourceTransformers() { return this.resourceTransformers; } + /** + * Configure the {@link ResourceHttpMessageWriter} to use. + *

By default a {@link ResourceHttpMessageWriter} will be configured. + */ + public void setResourceHttpMessageWriter(@Nullable ResourceHttpMessageWriter httpMessageWriter) { + this.resourceHttpMessageWriter = httpMessageWriter; + } + + /** + * Return the configured resource message writer. + */ + @Nullable + public ResourceHttpMessageWriter getResourceHttpMessageWriter() { + return this.resourceHttpMessageWriter; + } + /** * Set the {@link org.springframework.http.CacheControl} instance to build * the Cache-Control HTTP response header. @@ -230,19 +257,48 @@ public CacheControl getCacheControl() { } /** - * Configure the {@link ResourceHttpMessageWriter} to use. - *

By default a {@link ResourceHttpMessageWriter} will be configured. + * 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. + * @since 5.3 */ - public void setResourceHttpMessageWriter(@Nullable ResourceHttpMessageWriter httpMessageWriter) { - this.resourceHttpMessageWriter = httpMessageWriter; + public void setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; } /** - * Return the configured resource message writer. + * Return whether the {@link Resource#lastModified()} information is used + * to drive HTTP responses when serving static resources. + * @since 5.3 */ - @Nullable - public ResourceHttpMessageWriter getResourceHttpMessageWriter() { - return this.resourceHttpMessageWriter; + public boolean isUseLastModified() { + return this.useLastModified; + } + + /** + * Set whether to optimize the specified locations through an existence + * check on startup, filtering non-existing directories upfront so that + * they do not have to be checked on every resource access. + *

The default is {@code false}, for defensiveness against zip files + * without directory entries which are unable to expose the existence of + * a directory upfront. Switch this flag to {@code true} for optimized + * access in case of a consistent jar layout with directory entries. + * @since 5.3.13 + */ + public void setOptimizeLocations(boolean optimizeLocations) { + this.optimizeLocations = optimizeLocations; + } + + /** + * Return whether to optimize the specified locations through an existence + * check on startup, filtering non-existing directories upfront so that + * they do not have to be checked on every resource access. + * @since 5.3.13 + */ + public boolean isOptimizeLocations() { + return this.optimizeLocations; } /** @@ -269,36 +325,6 @@ public Map getMediaTypes() { return (this.mediaTypes != null ? this.mediaTypes : Collections.emptyMap()); } - /** - * Provide the ResourceLoader to load {@link #setLocationValues(List) - * location values} with. - * @since 5.1 - */ - public void setResourceLoader(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. - *

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 { @@ -332,7 +358,9 @@ private void resolveResourceLocations() { } } - result = result.stream().filter(Resource::exists).collect(Collectors.toList()); + if (isOptimizeLocations()) { + result = result.stream().filter(Resource::exists).collect(Collectors.toList()); + } this.locationsToUse.clear(); this.locationsToUse.addAll(result); 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 b668172197d6..c213b1672ac4 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 @@ -74,6 +74,7 @@ public class ResourceWebHandlerTests { private ResourceWebHandler handler; + @BeforeEach public void setup() throws Exception { List locations = new ArrayList<>(2); @@ -253,7 +254,7 @@ public void getResourceFromFileSystem() throws Exception { assertResponseBody(exchange, "h1 { color:red; }"); } - @Test // gh-27538 + @Test // gh-27538, gh-27624 public void filterNonExistingLocations() throws Exception { List inputLocations = Arrays.asList( new ClassPathResource("test/", getClass()), @@ -262,6 +263,7 @@ public void filterNonExistingLocations() throws Exception { ResourceWebHandler handler = new ResourceWebHandler(); handler.setLocations(inputLocations); + handler.setOptimizeLocations(true); handler.afterPropertiesSet(); List actual = handler.getLocations(); 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 fd822bedd55b..c173f602455e 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 @@ -55,6 +55,8 @@ public class ResourceHandlerRegistration { private boolean useLastModified = true; + private boolean optimizeLocations = false; + /** * Create a {@link ResourceHandlerRegistration} instance. @@ -130,15 +132,33 @@ public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) { /** * 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. + * @param useLastModified whether the "last modified" resource information should be used * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation * @since 5.3 + * @see ResourceHttpRequestHandler#setUseLastModified */ public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) { this.useLastModified = useLastModified; return this; } + /** + * Set whether to optimize the specified locations through an existence check on startup, + * filtering non-existing directories upfront so that they do not have to be checked + * on every resource access. + *

The default is {@code false}, for defensiveness against zip files without directory + * entries which are unable to expose the existence of a directory upfront. Switch this flag to + * {@code true} for optimized access in case of a consistent jar layout with directory entries. + * @param optimizeLocations whether to optimize the locations through an existence check on startup + * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation + * @since 5.3.13 + * @see ResourceHttpRequestHandler#setOptimizeLocations + */ + public ResourceHandlerRegistration setOptimizeLocations(boolean optimizeLocations) { + this.optimizeLocations = optimizeLocations; + 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. @@ -204,6 +224,7 @@ else if (this.cachePeriod != null) { handler.setCacheSeconds(this.cachePeriod); } handler.setUseLastModified(this.useLastModified); + handler.setOptimizeLocations(this.optimizeLocations); 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 22f06ccf58d7..7855a1171cb3 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,11 +140,13 @@ public class ResourceHttpRequestHandler extends WebContentGenerator @Nullable private UrlPathHelper urlPathHelper; + private boolean useLastModified = true; + + private boolean optimizeLocations = false; + @Nullable private StringValueResolver embeddedValueResolver; - private boolean useLastModified = true; - public ResourceHttpRequestHandler() { super(HttpMethod.GET.name(), HttpMethod.HEAD.name()); @@ -185,13 +187,13 @@ public void setLocations(List locations) { /** * Return the configured {@code List} of {@code Resource} locations including * both String-based locations provided via - * {@link #setLocationValues(List) setLocationValues} and pre-resolved {@code Resource} - * locations provided via {@link #setLocations(List) setLocations}. + * {@link #setLocationValues(List) setLocationValues} and pre-resolved + * {@code Resource} locations provided via {@link #setLocations(List) setLocations}. *

Note that the returned list is fully initialized only after * initialization via {@link #afterPropertiesSet()}. - *

Note: As of 5.3.11 the list of locations is filtered - * to exclude those that don't actually exist and therefore the list returned - * from this method may be a subset of all given locations. + *

Note: As of 5.3.11 the list of locations may be filtered to + * exclude those that don't actually exist and therefore the list returned from this + * method may be a subset of all given locations. See {@link #setOptimizeLocations}. * @see #setLocationValues * @see #setLocations */ @@ -293,7 +295,7 @@ public void setContentNegotiationManager(@Nullable ContentNegotiationManager con /** * Return the configured content negotiation manager. * @since 4.3 - * @deprecated as of 5.2.4. + * @deprecated as of 5.2.4 */ @Nullable @Deprecated @@ -303,7 +305,7 @@ public ContentNegotiationManager getContentNegotiationManager() { /** * Add mappings between file extensions, extracted from the filename of a - * static {@link Resource}, and corresponding media type to set on the + * static {@link Resource}, and corresponding media type to set on the * response. *

Use of this method is typically not necessary since mappings are * otherwise determined via @@ -361,9 +363,16 @@ public UrlPathHelper getUrlPathHelper() { return this.urlPathHelper; } - @Override - public void setEmbeddedValueResolver(StringValueResolver resolver) { - this.embeddedValueResolver = resolver; + /** + * 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. + * @since 5.3 + */ + public void setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; } /** @@ -376,18 +385,35 @@ public boolean isUseLastModified() { } /** - * 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 + * Set whether to optimize the specified locations through an existence + * check on startup, filtering non-existing directories upfront so that + * they do not have to be checked on every resource access. + *

The default is {@code false}, for defensiveness against zip files + * without directory entries which are unable to expose the existence of + * a directory upfront. Switch this flag to {@code true} for optimized + * access in case of a consistent jar layout with directory entries. + * @since 5.3.13 */ - public void setUseLastModified(boolean useLastModified) { - this.useLastModified = useLastModified; + public void setOptimizeLocations(boolean optimizeLocations) { + this.optimizeLocations = optimizeLocations; + } + + /** + * Return whether to optimize the specified locations through an existence + * check on startup, filtering non-existing directories upfront so that + * they do not have to be checked on every resource access. + * @since 5.3.13 + */ + public boolean isOptimizeLocations() { + return this.optimizeLocations; } + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + + @Override public void afterPropertiesSet() throws Exception { resolveResourceLocations(); @@ -449,8 +475,8 @@ private void resolveResourceLocations() { if (location.equals("/") && !(resource instanceof ServletContextResource)) { throw new IllegalStateException( "The String-based location \"/\" should be relative to the web application root " + - "but resolved to a Resource of type: " + resource.getClass() + ". " + - "If this is intentional, please pass it as a pre-configured Resource via setLocations."); + "but resolved to a Resource of type: " + resource.getClass() + ". " + + "If this is intentional, please pass it as a pre-configured Resource via setLocations."); } result.add(resource); if (charset != null) { @@ -463,7 +489,9 @@ private void resolveResourceLocations() { } result.addAll(this.locationResources); - result = result.stream().filter(Resource::exists).collect(Collectors.toList()); + if (isOptimizeLocations()) { + result = result.stream().filter(Resource::exists).collect(Collectors.toList()); + } this.locationsToUse.clear(); this.locationsToUse.addAll(result); @@ -508,6 +536,7 @@ protected org.springframework.web.accept.PathExtensionContentNegotiationStrategy return null; } + /** * Processes a resource request. *

Checks for the existence of the requested resource in the configured list of locations. 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 9aefd7f9d07b..834aab2694a6 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 @@ -311,7 +311,7 @@ public String getMimeType(String filePath) { assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }"); } - @Test // gh-27538 + @Test // gh-27538, gh-27624 public void filterNonExistingLocations() throws Exception { List inputLocations = Arrays.asList( new ClassPathResource("test/", getClass()), @@ -321,6 +321,7 @@ public void filterNonExistingLocations() throws Exception { ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler(); handler.setServletContext(new MockServletContext()); handler.setLocations(inputLocations); + handler.setOptimizeLocations(true); handler.afterPropertiesSet(); List actual = handler.getLocations(); diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 3b601ea4c885..1cc302102126 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -4170,8 +4170,8 @@ the example: @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**") - .addResourceLocations("/public", "classpath:/static/") - .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); } } @@ -4259,6 +4259,9 @@ re-write URLs to include the version of the jar and can also match against incom without versions -- for example, from `/jquery/jquery.min.js` to `/jquery/1.2.0/jquery.min.js`. +TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options +for fine-grained control, e.g. last-modified behavior and optimized resource resolution. + [[webflux-config-path-matching]] diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index b49666303e98..33b49bef56a5 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -5738,8 +5738,8 @@ The following listing shows how to do so with Java configuration: @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**") - .addResourceLocations("/public", "classpath:/static/") - .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); } } ---- @@ -5846,6 +5846,9 @@ re-write URLs to include the version of the jar and can also match against incom without versions -- for example, from `/jquery/jquery.min.js` to `/jquery/1.2.0/jquery.min.js`. +TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options +for fine-grained control, e.g. last-modified behavior and optimized resource resolution. + [[mvc-default-servlet-handler]]