Skip to content

Commit

Permalink
Introduce optimizeLocations flag for resource location filtering on s…
Browse files Browse the repository at this point in the history
…tartup

This flag is off by default since it requires jar files with directory entries.

Closes gh-27624
  • Loading branch information
jhoeller committed Nov 10, 2021
1 parent 11a0df3 commit 2a26870
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 80 deletions.
Expand Up @@ -54,6 +54,8 @@ public class ResourceHandlerRegistration {

private boolean useLastModified = true;

private boolean optimizeLocations = false;

@Nullable
private Map<String, MediaType> mediaTypes;

Expand Down Expand Up @@ -105,15 +107,33 @@ public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) {
/**
* 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.
* @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.
* <p>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.
Expand Down Expand Up @@ -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());
Expand All @@ -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);
}
Expand Down
Expand Up @@ -72,8 +72,8 @@
* <p>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.
Expand All @@ -85,6 +85,7 @@
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @author Juergen Hoeller
* @since 5.0
*/
public class ResourceWebHandler implements WebHandler, InitializingBean {
Expand All @@ -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<String> locationValues = new ArrayList<>(4);

private final List<Resource> locationResources = new ArrayList<>(4);
Expand All @@ -119,11 +123,18 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
@Nullable
private Map<String, MediaType> 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
Expand Down Expand Up @@ -161,9 +172,9 @@ public void setLocations(@Nullable List<Resource> locations) {
* <p>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()}.
* <p><strong>Note:</strong> 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.
* <p><strong>Note:</strong> 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
*/
Expand Down Expand Up @@ -212,6 +223,22 @@ public List<ResourceTransformer> getResourceTransformers() {
return this.resourceTransformers;
}

/**
* Configure the {@link ResourceHttpMessageWriter} to use.
* <p>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.
Expand All @@ -230,19 +257,48 @@ public CacheControl getCacheControl() {
}

/**
* Configure the {@link ResourceHttpMessageWriter} to use.
* <p>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.
* <p>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.
* <p>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;
}

/**
Expand All @@ -269,36 +325,6 @@ public Map<String, MediaType> 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.
* <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
public void afterPropertiesSet() throws Exception {
Expand Down Expand Up @@ -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);
Expand Down
Expand Up @@ -74,6 +74,7 @@ public class ResourceWebHandlerTests {

private ResourceWebHandler handler;


@BeforeEach
public void setup() throws Exception {
List<Resource> locations = new ArrayList<>(2);
Expand Down Expand Up @@ -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<Resource> inputLocations = Arrays.asList(
new ClassPathResource("test/", getClass()),
Expand All @@ -262,6 +263,7 @@ public void filterNonExistingLocations() throws Exception {

ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocations(inputLocations);
handler.setOptimizeLocations(true);
handler.afterPropertiesSet();

List<Resource> actual = handler.getLocations();
Expand Down
Expand Up @@ -55,6 +55,8 @@ public class ResourceHandlerRegistration {

private boolean useLastModified = true;

private boolean optimizeLocations = false;


/**
* Create a {@link ResourceHandlerRegistration} instance.
Expand Down Expand Up @@ -130,15 +132,33 @@ public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) {
/**
* 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.
* @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.
* <p>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.
Expand Down Expand Up @@ -204,6 +224,7 @@ else if (this.cachePeriod != null) {
handler.setCacheSeconds(this.cachePeriod);
}
handler.setUseLastModified(this.useLastModified);
handler.setOptimizeLocations(this.optimizeLocations);
return handler;
}

Expand Down

0 comments on commit 2a26870

Please sign in to comment.