Remove Servlet-specific static locations

This commit removes the Servlet root context from the default values for
the `spring.resources.static-locations` configuration property. Servlet
and non-Servlet applications are sharing this property.

The Servlet root context is automatically configured as a resource
location for Spring MVC based applications.

Closes gh-9240
This commit is contained in:
Brian Clozel 2017-09-13 15:18:33 +02:00
parent eb4a9d87fd
commit 9dd3fb70e2
5 changed files with 79 additions and 89 deletions

View File

@ -16,15 +16,7 @@
package org.springframework.boot.autoconfigure.web; package org.springframework.boot.autoconfigure.web;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
/** /**
* Properties used to configure resource handling. * Properties used to configure resource handling.
@ -36,30 +28,17 @@ import org.springframework.core.io.ResourceLoader;
* @since 1.1.0 * @since 1.1.0
*/ */
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware { public class ResourceProperties {
private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" };
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/META-INF/resources/", "classpath:/resources/",
"classpath:/static/", "classpath:/public/" }; "classpath:/static/", "classpath:/public/" };
private static final String[] RESOURCE_LOCATIONS;
static {
RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length
+ SERVLET_RESOURCE_LOCATIONS.length];
System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0,
SERVLET_RESOURCE_LOCATIONS.length);
System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS,
SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length);
}
/** /**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/, * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/] plus context:/ (the root of the servlet context). * /resources/, /static/, /public/].
*/ */
private String[] staticLocations = RESOURCE_LOCATIONS; private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
/** /**
* Cache period for the resources served by the resource handler, in seconds. * Cache period for the resources served by the resource handler, in seconds.
@ -73,12 +52,6 @@ public class ResourceProperties implements ResourceLoaderAware {
private final Chain chain = new Chain(); private final Chain chain = new Chain();
private ResourceLoader resourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public String[] getStaticLocations() { public String[] getStaticLocations() {
return this.staticLocations; return this.staticLocations;
@ -97,45 +70,6 @@ public class ResourceProperties implements ResourceLoaderAware {
return normalized; return normalized;
} }
public Resource getWelcomePage() {
for (String location : getStaticWelcomePageLocations()) {
Resource resource = this.resourceLoader.getResource(location);
try {
if (resource.exists()) {
resource.getURL();
return resource;
}
}
catch (Exception ex) {
// Ignore
}
}
return null;
}
private String[] getStaticWelcomePageLocations() {
String[] result = new String[this.staticLocations.length];
for (int i = 0; i < result.length; i++) {
String location = this.staticLocations[i];
if (!location.endsWith("/")) {
location = location + "/";
}
result[i] = location + "index.html";
}
return result;
}
public List<Resource> resolveFaviconLocations() {
List<Resource> locations = new ArrayList<>(this.staticLocations.length + 1);
if (this.resourceLoader != null) {
for (String location : this.staticLocations) {
locations.add(this.resourceLoader.getResource(location));
}
}
locations.add(new ClassPathResource("/"));
return Collections.unmodifiableList(locations);
}
public Integer getCachePeriod() { public Integer getCachePeriod() {
return this.cachePeriod; return this.cachePeriod;
} }

View File

@ -16,6 +16,8 @@
package org.springframework.boot.autoconfigure.web.servlet; package org.springframework.boot.autoconfigure.web.servlet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
@ -23,6 +25,7 @@ import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional;
import javax.servlet.Servlet; import javax.servlet.Servlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -54,6 +57,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter; import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter; import org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter;
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter; import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -62,7 +66,9 @@ import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.format.Formatter; import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry; import org.springframework.format.FormatterRegistry;
import org.springframework.format.datetime.DateFormatter; import org.springframework.format.datetime.DateFormatter;
@ -140,6 +146,8 @@ public class WebMvcAutoConfiguration {
public static final String DEFAULT_SUFFIX = ""; public static final String DEFAULT_SUFFIX = "";
private static final String[] SERVLET_LOCATIONS = { "/" };
@Bean @Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
@ -158,7 +166,7 @@ public class WebMvcAutoConfiguration {
@Configuration @Configuration
@Import(EnableWebMvcConfiguration.class) @Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ResourceLoaderAware {
private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class); private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);
@ -172,6 +180,8 @@ public class WebMvcAutoConfiguration {
final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
private ResourceLoader resourceLoader;
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties,
WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory,
@Lazy HttpMessageConverters messageConverters, @Lazy HttpMessageConverters messageConverters,
@ -184,6 +194,11 @@ public class WebMvcAutoConfiguration {
.getIfAvailable(); .getIfAvailable();
} }
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override @Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.addAll(this.messageConverters.getConverters()); converters.addAll(this.messageConverters.getConverters());
@ -302,18 +317,43 @@ public class WebMvcAutoConfiguration {
customizeResourceHandlerRegistration( customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern) registry.addResourceHandler(staticPathPattern)
.addResourceLocations( .addResourceLocations(
this.resourceProperties.getStaticLocations()) getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(cachePeriod)); .setCachePeriod(cachePeriod));
} }
} }
@Bean @Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping( public WelcomePageHandlerMapping welcomePageHandlerMapping() {
ResourceProperties resourceProperties) { return new WelcomePageHandlerMapping(getWelcomePage(),
return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(),
this.mvcProperties.getStaticPathPattern()); this.mvcProperties.getStaticPathPattern());
} }
static String[] getResourceLocations(String[] staticLocations) {
String[] locations = new String[staticLocations.length + SERVLET_LOCATIONS.length];
System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length);
System.arraycopy(SERVLET_LOCATIONS, 0, locations,
staticLocations.length, SERVLET_LOCATIONS.length);
return locations;
}
private Optional<Resource> getWelcomePage() {
return Arrays.stream(getResourceLocations(this.resourceProperties.getStaticLocations()))
.map(location -> this.resourceLoader.getResource(location + "index.html"))
.filter(resource -> {
try {
if (resource.exists()) {
resource.getURL();
return true;
}
}
catch (Exception ex) {
// Ignore
}
return false;
})
.findFirst();
}
private void customizeResourceHandlerRegistration( private void customizeResourceHandlerRegistration(
ResourceHandlerRegistration registration) { ResourceHandlerRegistration registration) {
if (this.resourceHandlerRegistrationCustomizer != null) { if (this.resourceHandlerRegistrationCustomizer != null) {
@ -331,14 +371,21 @@ public class WebMvcAutoConfiguration {
@Configuration @Configuration
@ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true) @ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
public static class FaviconConfiguration { public static class FaviconConfiguration implements ResourceLoaderAware {
private final ResourceProperties resourceProperties; private final ResourceProperties resourceProperties;
private ResourceLoader resourceLoader;
public FaviconConfiguration(ResourceProperties resourceProperties) { public FaviconConfiguration(ResourceProperties resourceProperties) {
this.resourceProperties = resourceProperties; this.resourceProperties = resourceProperties;
} }
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Bean @Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() { public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
@ -351,11 +398,19 @@ public class WebMvcAutoConfiguration {
@Bean @Bean
public ResourceHttpRequestHandler faviconRequestHandler() { public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
requestHandler requestHandler.setLocations(resolveFaviconLocations());
.setLocations(this.resourceProperties.resolveFaviconLocations());
return requestHandler; return requestHandler;
} }
private List<Resource> resolveFaviconLocations() {
String[] resourceLocations = getResourceLocations(this.resourceProperties.getStaticLocations());
List<Resource> locations = new ArrayList<>(resourceLocations.length + 1);
Arrays.stream(resourceLocations)
.forEach(location -> locations.add(this.resourceLoader.getResource(location)));
locations.add(new ClassPathResource("/"));
return Collections.unmodifiableList(locations);
}
} }
} }
@ -546,9 +601,9 @@ public class WebMvcAutoConfiguration {
private static final Log logger = LogFactory private static final Log logger = LogFactory
.getLog(WelcomePageHandlerMapping.class); .getLog(WelcomePageHandlerMapping.class);
private WelcomePageHandlerMapping(Resource welcomePage, private WelcomePageHandlerMapping(Optional<Resource> welcomePage,
String staticPathPattern) { String staticPathPattern) {
if (welcomePage != null && "/**".equals(staticPathPattern)) { if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage); logger.info("Adding welcome page: " + welcomePage);
ParameterizableViewController controller = new ParameterizableViewController(); ParameterizableViewController controller = new ParameterizableViewController();
controller.setViewName("forward:index.html"); controller.setViewName("forward:index.html");

View File

@ -127,7 +127,7 @@ public class WebFluxAutoConfigurationTests {
SimpleUrlHandlerMapping.class); SimpleUrlHandlerMapping.class);
assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class); assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**"); ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**");
assertThat(staticHandler.getLocations()).hasSize(5); assertThat(staticHandler.getLocations()).hasSize(4);
assertThat(hm.getUrlMap().get("/webjars/**")) assertThat(hm.getUrlMap().get("/webjars/**"))
.isInstanceOf(ResourceWebHandler.class); .isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler webjarsHandler = (ResourceWebHandler) hm.getUrlMap() ResourceWebHandler webjarsHandler = (ResourceWebHandler) hm.getUrlMap()
@ -146,7 +146,7 @@ public class WebFluxAutoConfigurationTests {
.isInstanceOf(ResourceWebHandler.class); .isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap() ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap()
.get("/static/**"); .get("/static/**");
assertThat(staticHandler.getLocations()).hasSize(5); assertThat(staticHandler.getLocations()).hasSize(4);
} }
@Test @Test

View File

@ -442,7 +442,7 @@ public class WebMvcAutoConfigurationTests {
.withPropertyValues("spring.resources.static-locations=classpath:/static") .withPropertyValues("spring.resources.static-locations=classpath:/static")
.run((context) -> assertThat( .run((context) -> assertThat(
getFaviconMappingLocations(context).get("/**/favicon.ico")) getFaviconMappingLocations(context).get("/**/favicon.ico"))
.hasSize(2)); .hasSize(3));
} }
@Test @Test

View File

@ -1900,10 +1900,11 @@ can be achieved as follows:
---- ----
You can also customize the static resource locations using You can also customize the static resource locations using
`spring.resources.static-locations` (replacing the default values with a list of directory `spring.resources.static-locations` (replacing the default values with a list
locations). If you do this the default welcome page detection will switch to your custom of directory locations). The root Servlet context path `"/"` will be automatically
locations, so if there is an `index.html` in any of your locations on startup, it will be added as a location as well. If you do this the default welcome page detection will
the home page of the application. switch to your custom locations, so if there is an `index.html` in any of your locations
on startup, it will be the home page of the application.
In addition to the '`standard`' static resource locations above, a special case is made In addition to the '`standard`' static resource locations above, a special case is made
for http://www.webjars.org/[Webjars content]. Any resources with a path in `+/webjars/**+` for http://www.webjars.org/[Webjars content]. Any resources with a path in `+/webjars/**+`
@ -2249,7 +2250,7 @@ Unlike Spring MVC, it does not require the Servlet API, is fully asynchronous an
non-blocking, and implements the http://www.reactive-streams.org/[Reactive Streams] non-blocking, and implements the http://www.reactive-streams.org/[Reactive Streams]
specification through http://projectreactor.io/[the Reactor project]. specification through http://projectreactor.io/[the Reactor project].
Spring WebFlux comes in two flavors the annotation-based one is quite close to the Spring WebFlux comes in two flavors; the annotation-based one is quite close to the
Spring MVC model we know: Spring MVC model we know:
[source,java,indent=0] [source,java,indent=0]
@ -3066,7 +3067,7 @@ auto-configured. In this example it's pulled in transitively via
`spring-boot-starter-data-jpa`. `spring-boot-starter-data-jpa`.
TIP: If, for whatever reason, you do configure the connection URL for an embedded TIP: If, for whatever reason, you do configure the connection URL for an embedded
database, care should be taken to ensure that the databases automatic shutdown is database, care should be taken to ensure that the database's automatic shutdown is
disabled. If you're using H2 you should use `DB_CLOSE_ON_EXIT=FALSE` to do so. If you're disabled. If you're using H2 you should use `DB_CLOSE_ON_EXIT=FALSE` to do so. If you're
using HSQLDB, you should ensure that `shutdown=true` is not used. Disabling the database's using HSQLDB, you should ensure that `shutdown=true` is not used. Disabling the database's
automatic shutdown allows Spring Boot to control when the database is closed, thereby automatic shutdown allows Spring Boot to control when the database is closed, thereby
@ -4573,7 +4574,7 @@ recommend to keep this setting enabled if you create your own `RedisCacheManager
[[boot-features-caching-provider-caffeine]] [[boot-features-caching-provider-caffeine]]
==== Caffeine ==== Caffeine
Caffeine is a Java 8 rewrite of Guavas cache that supersede the Guava support. If Caffeine is a Java 8 rewrite of Guava's cache that supersede the Guava support. If
Caffeine is present, a `CaffeineCacheManager` (provided by the Caffeine is present, a `CaffeineCacheManager` (provided by the
`spring-boot-starter-cache` '`Starter`') is auto-configured. Caches can be created `spring-boot-starter-cache` '`Starter`') is auto-configured. Caches can be created
on startup using the `spring.cache.cache-names` property and customized by one of the on startup using the `spring.cache.cache-names` property and customized by one of the