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
This commit is contained in:
Juergen Hoeller 2021-11-10 14:21:48 +01:00
parent 11a0df3fd1
commit 2a268701c4
8 changed files with 188 additions and 80 deletions

View File

@ -54,6 +54,8 @@ public class ResourceHandlerRegistration {
private boolean useLastModified = true; private boolean useLastModified = true;
private boolean optimizeLocations = false;
@Nullable @Nullable
private Map<String, MediaType> mediaTypes; private Map<String, MediaType> mediaTypes;
@ -105,15 +107,33 @@ public class ResourceHandlerRegistration {
/** /**
* Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses. * Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses.
* <p>This configuration is set to {@code true} by default. * <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 * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
* @since 5.3 * @since 5.3
* @see ResourceWebHandler#setUseLastModified
*/ */
public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) { public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified; this.useLastModified = useLastModified;
return this; 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 * 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.
@ -181,8 +201,8 @@ public class ResourceHandlerRegistration {
*/ */
protected ResourceWebHandler getRequestHandler() { protected ResourceWebHandler getRequestHandler() {
ResourceWebHandler handler = new ResourceWebHandler(); ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocationValues(this.locationValues);
handler.setResourceLoader(this.resourceLoader); handler.setResourceLoader(this.resourceLoader);
handler.setLocationValues(this.locationValues);
if (this.resourceChainRegistration != null) { if (this.resourceChainRegistration != null) {
handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers()); handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers());
handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers()); handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
@ -191,6 +211,7 @@ public class ResourceHandlerRegistration {
handler.setCacheControl(this.cacheControl); handler.setCacheControl(this.cacheControl);
} }
handler.setUseLastModified(this.useLastModified); handler.setUseLastModified(this.useLastModified);
handler.setOptimizeLocations(this.optimizeLocations);
if (this.mediaTypes != null) { if (this.mediaTypes != null) {
handler.setMediaTypes(this.mediaTypes); handler.setMediaTypes(this.mediaTypes);
} }

View File

@ -72,8 +72,8 @@ import org.springframework.web.server.WebHandler;
* <p>This request handler may also be configured with a * <p>This request handler may also be configured with a
* {@link #setResourceResolvers(List) resourcesResolver} and * {@link #setResourceResolvers(List) resourcesResolver} and
* {@link #setResourceTransformers(List) resourceTransformer} chains to support * {@link #setResourceTransformers(List) resourceTransformer} chains to support
* arbitrary resolution and transformation of resources being served. By default a * arbitrary resolution and transformation of resources being served. By default
* {@link PathResourceResolver} simply finds resources based on the configured * a {@link PathResourceResolver} simply finds resources based on the configured
* "locations". An application can configure additional resolvers and * "locations". An application can configure additional resolvers and
* transformers such as the {@link VersionResourceResolver} which can resolve * transformers such as the {@link VersionResourceResolver} which can resolve
* and prepare URLs for resources with a version in the URL. * and prepare URLs for resources with a version in the URL.
@ -85,6 +85,7 @@ import org.springframework.web.server.WebHandler;
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Brian Clozel * @author Brian Clozel
* @author Juergen Hoeller
* @since 5.0 * @since 5.0
*/ */
public class ResourceWebHandler implements WebHandler, InitializingBean { 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); private static final Log logger = LogFactory.getLog(ResourceWebHandler.class);
@Nullable
private ResourceLoader resourceLoader;
private final List<String> locationValues = new ArrayList<>(4); private final List<String> locationValues = new ArrayList<>(4);
private final List<Resource> locationResources = new ArrayList<>(4); private final List<Resource> locationResources = new ArrayList<>(4);
@ -119,11 +123,18 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
@Nullable @Nullable
private Map<String, MediaType> mediaTypes; private Map<String, MediaType> mediaTypes;
@Nullable
private ResourceLoader resourceLoader;
private boolean useLastModified = true; 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 * Accepts a list of String-based location values to be resolved into
@ -161,9 +172,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
* <p>Note that if {@link #setLocationValues(List) locationValues} are provided, * <p>Note that if {@link #setLocationValues(List) locationValues} are provided,
* instead of loaded Resource-based locations, this method will return * instead of loaded Resource-based locations, this method will return
* empty until after initialization via {@link #afterPropertiesSet()}. * empty until after initialization via {@link #afterPropertiesSet()}.
* <p><strong>Note:</strong> As of 5.3.11 the list of locations is filtered * <p><strong>Note:</strong> As of 5.3.11 the list of locations may be filtered to
* to exclude those that don't actually exist and therefore the list returned * exclude those that don't actually exist and therefore the list returned from this
* from this method may be a subset of all given locations. * method may be a subset of all given locations. See {@link #setOptimizeLocations}.
* @see #setLocationValues * @see #setLocationValues
* @see #setLocations * @see #setLocations
*/ */
@ -212,6 +223,22 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
return this.resourceTransformers; 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 * Set the {@link org.springframework.http.CacheControl} instance to build
* the Cache-Control HTTP response header. * the Cache-Control HTTP response header.
@ -230,19 +257,48 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
} }
/** /**
* Configure the {@link ResourceHttpMessageWriter} to use. * Set whether we should look at the {@link Resource#lastModified()}
* <p>By default a {@link ResourceHttpMessageWriter} will be configured. * 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) { public void setUseLastModified(boolean useLastModified) {
this.resourceHttpMessageWriter = httpMessageWriter; 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 boolean isUseLastModified() {
public ResourceHttpMessageWriter getResourceHttpMessageWriter() { return this.useLastModified;
return this.resourceHttpMessageWriter; }
/**
* 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;
} }
/** /**
@ -269,36 +325,6 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
return (this.mediaTypes != null ? this.mediaTypes : Collections.emptyMap()); 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 @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
@ -332,7 +358,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
} }
} }
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.clear();
this.locationsToUse.addAll(result); this.locationsToUse.addAll(result);

View File

@ -74,6 +74,7 @@ public class ResourceWebHandlerTests {
private ResourceWebHandler handler; private ResourceWebHandler handler;
@BeforeEach @BeforeEach
public void setup() throws Exception { public void setup() throws Exception {
List<Resource> locations = new ArrayList<>(2); List<Resource> locations = new ArrayList<>(2);
@ -253,7 +254,7 @@ public class ResourceWebHandlerTests {
assertResponseBody(exchange, "h1 { color:red; }"); assertResponseBody(exchange, "h1 { color:red; }");
} }
@Test // gh-27538 @Test // gh-27538, gh-27624
public void filterNonExistingLocations() throws Exception { public void filterNonExistingLocations() throws Exception {
List<Resource> inputLocations = Arrays.asList( List<Resource> inputLocations = Arrays.asList(
new ClassPathResource("test/", getClass()), new ClassPathResource("test/", getClass()),
@ -262,6 +263,7 @@ public class ResourceWebHandlerTests {
ResourceWebHandler handler = new ResourceWebHandler(); ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocations(inputLocations); handler.setLocations(inputLocations);
handler.setOptimizeLocations(true);
handler.afterPropertiesSet(); handler.afterPropertiesSet();
List<Resource> actual = handler.getLocations(); List<Resource> actual = handler.getLocations();

View File

@ -55,6 +55,8 @@ public class ResourceHandlerRegistration {
private boolean useLastModified = true; private boolean useLastModified = true;
private boolean optimizeLocations = false;
/** /**
* Create a {@link ResourceHandlerRegistration} instance. * Create a {@link ResourceHandlerRegistration} instance.
@ -130,15 +132,33 @@ public class ResourceHandlerRegistration {
/** /**
* Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses. * Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses.
* <p>This configuration is set to {@code true} by default. * <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 * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
* @since 5.3 * @since 5.3
* @see ResourceHttpRequestHandler#setUseLastModified
*/ */
public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) { public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified; this.useLastModified = useLastModified;
return this; 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 * 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.
@ -204,6 +224,7 @@ public class ResourceHandlerRegistration {
handler.setCacheSeconds(this.cachePeriod); handler.setCacheSeconds(this.cachePeriod);
} }
handler.setUseLastModified(this.useLastModified); handler.setUseLastModified(this.useLastModified);
handler.setOptimizeLocations(this.optimizeLocations);
return handler; return handler;
} }

View File

@ -140,11 +140,13 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
@Nullable @Nullable
private UrlPathHelper urlPathHelper; private UrlPathHelper urlPathHelper;
private boolean useLastModified = true;
private boolean optimizeLocations = false;
@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());
@ -185,13 +187,13 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
/** /**
* Return the configured {@code List} of {@code Resource} locations including * Return the configured {@code List} of {@code Resource} locations including
* both String-based locations provided via * both String-based locations provided via
* {@link #setLocationValues(List) setLocationValues} and pre-resolved {@code Resource} * {@link #setLocationValues(List) setLocationValues} and pre-resolved
* locations provided via {@link #setLocations(List) setLocations}. * {@code Resource} locations provided via {@link #setLocations(List) setLocations}.
* <p>Note that the returned list is fully initialized only after * <p>Note that the returned list is fully initialized only after
* initialization via {@link #afterPropertiesSet()}. * initialization via {@link #afterPropertiesSet()}.
* <p><strong>Note:</strong> As of 5.3.11 the list of locations is filtered * <p><strong>Note:</strong> As of 5.3.11 the list of locations may be filtered to
* to exclude those that don't actually exist and therefore the list returned * exclude those that don't actually exist and therefore the list returned from this
* from this method may be a subset of all given locations. * method may be a subset of all given locations. See {@link #setOptimizeLocations}.
* @see #setLocationValues * @see #setLocationValues
* @see #setLocations * @see #setLocations
*/ */
@ -293,7 +295,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
/** /**
* Return the configured content negotiation manager. * Return the configured content negotiation manager.
* @since 4.3 * @since 4.3
* @deprecated as of 5.2.4. * @deprecated as of 5.2.4
*/ */
@Nullable @Nullable
@Deprecated @Deprecated
@ -303,7 +305,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
/** /**
* Add mappings between file extensions, extracted from the filename of a * 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. * response.
* <p>Use of this method is typically not necessary since mappings are * <p>Use of this method is typically not necessary since mappings are
* otherwise determined via * otherwise determined via
@ -361,9 +363,16 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return this.urlPathHelper; return this.urlPathHelper;
} }
@Override /**
public void setEmbeddedValueResolver(StringValueResolver resolver) { * Set whether we should look at the {@link Resource#lastModified()} when
this.embeddedValueResolver = resolver; * 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 setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified;
} }
/** /**
@ -376,18 +385,35 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
} }
/** /**
* Set whether we should look at the {@link Resource#lastModified()} * Set whether to optimize the specified locations through an existence
* when serving resources and use this information to drive {@code "Last-Modified"} * check on startup, filtering non-existing directories upfront so that
* HTTP response headers. * they do not have to be checked on every resource access.
* <p>This option is enabled by default and should be turned off if the metadata of * <p>The default is {@code false}, for defensiveness against zip files
* the static files should be ignored. * without directory entries which are unable to expose the existence of
* @param useLastModified whether to use the resource last-modified information. * a directory upfront. Switch this flag to {@code true} for optimized
* @since 5.3 * access in case of a consistent jar layout with directory entries.
* @since 5.3.13
*/ */
public void setUseLastModified(boolean useLastModified) { public void setOptimizeLocations(boolean optimizeLocations) {
this.useLastModified = useLastModified; 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 @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
resolveResourceLocations(); resolveResourceLocations();
@ -449,8 +475,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
if (location.equals("/") && !(resource instanceof ServletContextResource)) { if (location.equals("/") && !(resource instanceof ServletContextResource)) {
throw new IllegalStateException( throw new IllegalStateException(
"The String-based location \"/\" should be relative to the web application root " + "The String-based location \"/\" should be relative to the web application root " +
"but resolved to a Resource of type: " + resource.getClass() + ". " + "but resolved to a Resource of type: " + resource.getClass() + ". " +
"If this is intentional, please pass it as a pre-configured Resource via setLocations."); "If this is intentional, please pass it as a pre-configured Resource via setLocations.");
} }
result.add(resource); result.add(resource);
if (charset != null) { if (charset != null) {
@ -463,7 +489,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
} }
result.addAll(this.locationResources); 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.clear();
this.locationsToUse.addAll(result); this.locationsToUse.addAll(result);
@ -508,6 +536,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return null; return null;
} }
/** /**
* Processes a resource request. * Processes a resource request.
* <p>Checks for the existence of the requested resource in the configured list of locations. * <p>Checks for the existence of the requested resource in the configured list of locations.

View File

@ -311,7 +311,7 @@ public class ResourceHttpRequestHandlerTests {
assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }"); assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }");
} }
@Test // gh-27538 @Test // gh-27538, gh-27624
public void filterNonExistingLocations() throws Exception { public void filterNonExistingLocations() throws Exception {
List<Resource> inputLocations = Arrays.asList( List<Resource> inputLocations = Arrays.asList(
new ClassPathResource("test/", getClass()), new ClassPathResource("test/", getClass()),
@ -321,6 +321,7 @@ public class ResourceHttpRequestHandlerTests {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler(); ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setServletContext(new MockServletContext()); handler.setServletContext(new MockServletContext());
handler.setLocations(inputLocations); handler.setLocations(inputLocations);
handler.setOptimizeLocations(true);
handler.afterPropertiesSet(); handler.afterPropertiesSet();
List<Resource> actual = handler.getLocations(); List<Resource> actual = handler.getLocations();

View File

@ -4170,8 +4170,8 @@ the example:
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**") registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/") .addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); .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 without versions -- for example, from `/jquery/jquery.min.js` to
`/jquery/1.2.0/jquery.min.js`. `/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]] [[webflux-config-path-matching]]

View File

@ -5738,8 +5738,8 @@ The following listing shows how to do so with Java configuration:
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**") registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/") .addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); .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 without versions -- for example, from `/jquery/jquery.min.js` to
`/jquery/1.2.0/jquery.min.js`. `/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]] [[mvc-default-servlet-handler]]