From f9e3e0d532cbc6c4382f13440737d9a282734a1f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 4 Jan 2021 18:25:22 -0800 Subject: [PATCH] Register default resource path using a Resource Update `WebMvcAutoConfiguration` so that the default "/" resource path is registered directly as a `ServletContextResource`. Closes gh-24745 --- .../web/servlet/WebMvcAutoConfiguration.java | 174 +++++++++++------- .../servlet/WelcomePageHandlerMapping.java | 9 +- .../servlet/WebMvcAutoConfigurationTests.java | 4 +- .../WelcomePageHandlerMappingTests.java | 6 +- 4 files changed, 118 insertions(+), 75 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index b1b079a28d4d..02f4d34b6acf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,14 @@ package org.springframework.boot.autoconfigure.web.servlet; import java.time.Duration; -import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; -import java.util.Optional; +import java.util.Set; import javax.servlet.Servlet; +import javax.servlet.ServletContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -70,10 +71,10 @@ import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; -import org.springframework.http.CacheControl; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.util.ClassUtils; +import org.springframework.util.PathMatcher; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; @@ -85,11 +86,13 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextListener; +import org.springframework.web.context.support.ServletContextResource; import org.springframework.web.filter.FormContentFilter; import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.web.filter.RequestContextFilter; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; @@ -104,6 +107,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import org.springframework.web.servlet.i18n.FixedLocaleResolver; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; @@ -111,12 +115,14 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.resource.AppCacheManifestTransformer; import org.springframework.web.servlet.resource.EncodedResourceResolver; +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; import org.springframework.web.servlet.resource.ResourceResolver; import org.springframework.web.servlet.resource.ResourceUrlProvider; import org.springframework.web.servlet.resource.VersionResourceResolver; import org.springframework.web.servlet.view.BeanNameViewResolver; import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; import org.springframework.web.servlet.view.InternalResourceViewResolver; +import org.springframework.web.util.UrlPathHelper; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link EnableWebMvc Web MVC}. @@ -151,7 +157,7 @@ public class WebMvcAutoConfiguration { */ public static final String DEFAULT_SUFFIX = ""; - private static final String[] SERVLET_LOCATIONS = { "/" }; + private static final String SERVLET_LOCATION = "/"; @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @@ -167,13 +173,6 @@ public OrderedFormContentFilter formContentFilter() { return new OrderedFormContentFilter(); } - 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; - } - // Defined as a nested config to ensure WebMvcConfigurer is not read when not // on the classpath @Configuration(proxyBeanMethods = false) @@ -182,26 +181,17 @@ static String[] getResourceLocations(String[] staticLocations) { @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { - private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class); - - private final ResourceProperties resourceProperties; - private final WebMvcProperties mvcProperties; private final ListableBeanFactory beanFactory; private final ObjectProvider messageConvertersProvider; - final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; - - public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, - ListableBeanFactory beanFactory, ObjectProvider messageConvertersProvider, - ObjectProvider resourceHandlerRegistrationCustomizerProvider) { - this.resourceProperties = resourceProperties; + public WebMvcAutoConfigurationAdapter(WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, + ObjectProvider messageConvertersProvider) { this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; - this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); } @Override @@ -303,37 +293,6 @@ public void addFormatters(FormatterRegistry registry) { ApplicationConversionService.addBeans(registry, this.beanFactory); } - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - if (!this.resourceProperties.isAddMappings()) { - logger.debug("Default resource handling disabled"); - return; - } - Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); - CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); - if (!registry.hasMappingForPattern("/webjars/**")) { - customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**") - .addResourceLocations("classpath:/META-INF/resources/webjars/") - .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); - } - String staticPathPattern = this.mvcProperties.getStaticPathPattern(); - if (!registry.hasMappingForPattern(staticPathPattern)) { - customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern) - .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())) - .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); - } - } - - private Integer getSeconds(Duration cachePeriod) { - return (cachePeriod != null) ? (int) cachePeriod.getSeconds() : null; - } - - private void customizeResourceHandlerRegistration(ResourceHandlerRegistration registration) { - if (this.resourceHandlerRegistrationCustomizer != null) { - this.resourceHandlerRegistrationCustomizer.customize(registration); - } - } - @Bean @ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class }) @ConditionalOnMissingFilterBean(RequestContextFilter.class) @@ -349,22 +308,31 @@ public static RequestContextFilter requestContextFilter() { @Configuration(proxyBeanMethods = false) public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { + private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class); + private final ResourceProperties resourceProperties; private final WebMvcProperties mvcProperties; - private final ListableBeanFactory beanFactory; - private final WebMvcRegistrations mvcRegistrations; + private final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; + private ResourceLoader resourceLoader; + private final ListableBeanFactory beanFactory; + + private final Set autoConfiguredResourceHandlers = new HashSet<>(); + public EnableWebMvcConfiguration(ResourceProperties resourceProperties, ObjectProvider mvcPropertiesProvider, - ObjectProvider mvcRegistrationsProvider, ListableBeanFactory beanFactory) { + ObjectProvider mvcRegistrationsProvider, + ObjectProvider resourceHandlerRegistrationCustomizerProvider, + ListableBeanFactory beanFactory) { this.resourceProperties = resourceProperties; this.mvcProperties = mvcPropertiesProvider.getIfAvailable(); this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique(); + this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this.beanFactory = beanFactory; } @@ -401,6 +369,72 @@ public RequestMappingHandlerMapping requestMappingHandlerMapping( resourceUrlProvider); } + @Bean + @Override + public HandlerMapping resourceHandlerMapping(UrlPathHelper urlPathHelper, PathMatcher pathMatcher, + ContentNegotiationManager contentNegotiationManager, FormattingConversionService conversionService, + ResourceUrlProvider resourceUrlProvider) { + HandlerMapping mapping = super.resourceHandlerMapping(urlPathHelper, pathMatcher, contentNegotiationManager, + conversionService, resourceUrlProvider); + if (mapping instanceof SimpleUrlHandlerMapping) { + addServletContextResourceHandlerMapping((SimpleUrlHandlerMapping) mapping); + } + return mapping; + } + + private void addServletContextResourceHandlerMapping(SimpleUrlHandlerMapping mapping) { + Map urlMap = mapping.getUrlMap(); + String pattern = this.mvcProperties.getStaticPathPattern(); + Object handler = urlMap.get(pattern); + if (handler instanceof ResourceHttpRequestHandler + && this.autoConfiguredResourceHandlers.contains(pattern)) { + addServletContextResourceHandlerMapping((ResourceHttpRequestHandler) handler); + } + } + + private void addServletContextResourceHandlerMapping(ResourceHttpRequestHandler handler) { + ServletContext servletContext = getServletContext(); + if (servletContext != null) { + List locations = handler.getLocations(); + locations.add(new ServletContextResource(servletContext, SERVLET_LOCATION)); + } + } + + @Override + protected void addResourceHandlers(ResourceHandlerRegistry registry) { + super.addResourceHandlers(registry); + if (!this.resourceProperties.isAddMappings()) { + logger.debug("Default resource handling disabled"); + return; + } + addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); + addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), + this.resourceProperties.getStaticLocations()); + + } + + private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) { + if (registry.hasMappingForPattern(pattern)) { + return; + } + ResourceHandlerRegistration registration = registry.addResourceHandler(pattern); + registration.addResourceLocations(locations); + registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod())); + registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl()); + customizeResourceHandlerRegistration(registration); + this.autoConfiguredResourceHandlers.add(pattern); + } + + private Integer getSeconds(Duration cachePeriod) { + return (cachePeriod != null) ? (int) cachePeriod.getSeconds() : null; + } + + private void customizeResourceHandlerRegistration(ResourceHandlerRegistration registration) { + if (this.resourceHandlerRegistrationCustomizer != null) { + this.resourceHandlerRegistrationCustomizer.customize(registration); + } + } + @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { @@ -412,22 +446,34 @@ public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext ap return welcomePageHandlerMapping; } - private Optional getWelcomePage() { - String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations()); - return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst(); + private Resource getWelcomePage() { + for (String location : this.resourceProperties.getStaticLocations()) { + Resource indexHtml = getIndexHtml(location); + if (indexHtml != null) { + return indexHtml; + } + } + ServletContext servletContext = getServletContext(); + if (servletContext != null) { + return getIndexHtml(new ServletContextResource(servletContext, SERVLET_LOCATION)); + } + return null; } private Resource getIndexHtml(String location) { - return this.resourceLoader.getResource(location + "index.html"); + return getIndexHtml(this.resourceLoader.getResource(location)); } - private boolean isReadable(Resource resource) { + private Resource getIndexHtml(Resource location) { try { - return resource.exists() && (resource.getURL() != null); + Resource resource = location.createRelative("index.html"); + if (resource.exists() && (resource.getURL() != null)) { + return resource; + } } catch (Exception ex) { - return false; } + return null; } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java index 7717a8a548ed..2f33cbb3e9dc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.Collections; import java.util.List; -import java.util.Optional; import javax.servlet.http.HttpServletRequest; @@ -49,9 +48,9 @@ final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping { private static final List MEDIA_TYPES_ALL = Collections.singletonList(MediaType.ALL); WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, - ApplicationContext applicationContext, Optional welcomePage, String staticPathPattern) { - if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) { - logger.info("Adding welcome page: " + welcomePage.get()); + ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) { + if (welcomePage != null && "/**".equals(staticPathPattern)) { + logger.info("Adding welcome page: " + welcomePage); setRootViewName("forward:index.html"); } else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index 07890007967b..8c94102ab15f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -814,7 +814,7 @@ private void assertCacheControl(AssertableWebApplicationContext context) { protected Map> getResourceMappingLocations(ApplicationContext context) { Object bean = context.getBean("resourceHandlerMapping"); if (bean instanceof HandlerMapping) { - return getMappingLocations(context.getBean("resourceHandlerMapping", HandlerMapping.class)); + return getMappingLocations((HandlerMapping) bean); } assertThat(bean.toString()).isEqualTo("null"); return Collections.emptyMap(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMappingTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMappingTests.java index ede657075bfe..b907cdae2bec 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMappingTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.Collections; import java.util.Map; -import java.util.Optional; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -162,8 +161,7 @@ WelcomePageHandlerMapping handlerMapping(ApplicationContext applicationContext, return new WelcomePageHandlerMapping( templateAvailabilityProviders .getIfAvailable(() -> new TemplateAvailabilityProviders(applicationContext)), - applicationContext, Optional.ofNullable(staticIndexPage.getIfAvailable()), staticPathPattern); - + applicationContext, staticIndexPage.getIfAvailable(), staticPathPattern); } }