From 004363ceaf89ba329bcbb9bc613b4768dbb2134f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 20 May 2021 00:49:47 -0700 Subject: [PATCH] Use WebMvcConfigurer to add resource handlers Move resource handler auto-configuration logic back to the `WebMvcConfigurer` so that they also get applied to child contexts. Closes gh-25743 --- .../web/servlet/WebMvcAutoConfiguration.java | 219 ++++++++++++------ .../servlet/WebMvcAutoConfigurationTests.java | 69 ++++++ 2 files changed, 213 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 ad75bd3e5d..fa99c9092b 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 @@ -17,11 +17,10 @@ package org.springframework.boot.autoconfigure.web.servlet; import java.time.Duration; -import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; -import java.util.Set; import javax.servlet.Servlet; import javax.servlet.ServletContext; @@ -54,6 +53,7 @@ import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy; import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters; import org.springframework.boot.autoconfigure.web.format.WebConversionService; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.AutoConfigurationResourceHandlerRegistry.RegistrationType; import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Format; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.convert.ApplicationConversionService; @@ -76,6 +76,7 @@ import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.PathMatcher; import org.springframework.validation.DefaultMessageCodesResolver; @@ -110,6 +111,7 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry 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.AbstractHandlerMapping; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import org.springframework.web.servlet.i18n.FixedLocaleResolver; @@ -184,6 +186,10 @@ public class WebMvcAutoConfiguration { @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; @@ -194,13 +200,14 @@ public class WebMvcAutoConfiguration { private final ObjectProvider> servletRegistrations; - final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; + private final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; - public WebMvcAutoConfigurationAdapter(WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, - ObjectProvider messageConvertersProvider, + public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, + ListableBeanFactory beanFactory, ObjectProvider messageConvertersProvider, ObjectProvider resourceHandlerRegistrationCustomizerProvider, ObjectProvider dispatcherServletPath, ObjectProvider> servletRegistrations) { + this.resourceProperties = resourceProperties; this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; @@ -321,6 +328,39 @@ public class WebMvcAutoConfiguration { ApplicationConversionService.addBeans(registry, this.beanFactory); } + @Override + public void addResourceHandlers(ResourceHandlerRegistry 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 = AutoConfigurationResourceHandlerRegistry + .addResourceHandler(registry, RegistrationType.AUTO_CONFIGURATION, pattern); + registration.addResourceLocations(locations); + registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod())); + registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl()); + customizeResourceHandlerRegistration(registration); + } + + 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) @@ -336,31 +376,22 @@ public class WebMvcAutoConfiguration { @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 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, - ObjectProvider resourceHandlerRegistrationCustomizerProvider, - ListableBeanFactory beanFactory) { + ObjectProvider mvcRegistrationsProvider, ListableBeanFactory beanFactory) { this.resourceProperties = resourceProperties; this.mvcProperties = mvcPropertiesProvider.getIfAvailable(); this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique(); - this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this.beanFactory = beanFactory; } @@ -402,70 +433,28 @@ public class WebMvcAutoConfiguration { @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); + public HandlerMapping resourceHandlerMapping(@Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper, + @Qualifier("mvcPathMatcher") PathMatcher pathMatcher, + @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, + @Qualifier("mvcConversionService") FormattingConversionService conversionService, + @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { + Assert.state(getApplicationContext() != null, "No ApplicationContext set"); + Assert.state(getServletContext() != null, "No ServletContext set"); + AutoConfigurationResourceHandlerRegistry registry = new AutoConfigurationResourceHandlerRegistry( + getApplicationContext(), getServletContext(), contentNegotiationManager, urlPathHelper, + this.mvcProperties); + addResourceHandlers(registry); + AbstractHandlerMapping mapping = registry.getHandlerMapping(); + if (mapping == null) { + return null; } + mapping.setPathMatcher(pathMatcher); + mapping.setUrlPathHelper(urlPathHelper); + mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); + mapping.setCorsConfigurations(getCorsConfigurations()); 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) { @@ -677,4 +666,84 @@ public class WebMvcAutoConfiguration { } + /** + * {@link ResourceHandlerRegistry} that tracks auto-configuration and when appropriate + * adds the {@link ServletContextResource} for {@code /}. + */ + static class AutoConfigurationResourceHandlerRegistry + extends org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry { + + private final ServletContext servletContext; + + private final WebMvcProperties mvcProperties; + + private final Map registrations = new LinkedHashMap<>(); + + AutoConfigurationResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext, + ContentNegotiationManager contentNegotiationManager, UrlPathHelper pathHelper, + WebMvcProperties mvcProperties) { + super(applicationContext, servletContext, contentNegotiationManager, pathHelper); + this.servletContext = servletContext; + this.mvcProperties = mvcProperties; + } + + @Override + public ResourceHandlerRegistration addResourceHandler(String... pathPatterns) { + return addResourceHandler(RegistrationType.STANDARD, pathPatterns); + } + + ResourceHandlerRegistration addResourceHandler(RegistrationType type, String... pathPatterns) { + for (String pathPattern : pathPatterns) { + this.registrations.put(pathPattern, type); + } + return super.addResourceHandler(pathPatterns); + } + + @Override + protected AbstractHandlerMapping getHandlerMapping() { + SimpleUrlHandlerMapping mapping = (SimpleUrlHandlerMapping) super.getHandlerMapping(); + reconfigure(mapping); + return mapping; + } + + private void reconfigure(SimpleUrlHandlerMapping mapping) { + String staticPathPattern = this.mvcProperties.getStaticPathPattern(); + if (this.registrations.get(staticPathPattern) == RegistrationType.AUTO_CONFIGURATION) { + addServletContextResourceHandlerMapping(mapping, staticPathPattern); + } + } + + private void addServletContextResourceHandlerMapping(SimpleUrlHandlerMapping mapping, + String staticPathPattern) { + Object handler = mapping.getUrlMap().get(staticPathPattern); + if (handler instanceof ResourceHttpRequestHandler) { + addServletContextResourceHandlerMapping((ResourceHttpRequestHandler) handler); + } + } + + private void addServletContextResourceHandlerMapping(ResourceHttpRequestHandler handler) { + if (this.servletContext != null) { + List locations = handler.getLocations(); + locations.add(new ServletContextResource(this.servletContext, SERVLET_LOCATION)); + } + } + + static ResourceHandlerRegistration addResourceHandler(ResourceHandlerRegistry registry, RegistrationType type, + String... pathPatterns) { + if (registry instanceof AutoConfigurationResourceHandlerRegistry) { + return ((AutoConfigurationResourceHandlerRegistry) registry).addResourceHandler(type, pathPatterns); + } + return registry.addResourceHandler(pathPatterns); + } + + enum RegistrationType { + + STANDARD, + + AUTO_CONFIGURATION + + } + + } + } 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 c5aea2adce..06b9332f11 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 @@ -33,6 +33,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.ValidatorFactory; @@ -50,9 +51,11 @@ import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguratio import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.ApplicationContext; @@ -81,6 +84,7 @@ import org.springframework.web.accept.ParameterContentNegotiationStrategy; import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.FormContentFilter; import org.springframework.web.filter.HiddenHttpMethodFilter; @@ -95,6 +99,7 @@ import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; @@ -898,6 +903,25 @@ class WebMvcAutoConfigurationTests { }); } + @Test // gh-25743 + void addResourceHandlersAppliesToChildAndParentContext() { + try (AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext()) { + context.register(WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, + ResourceHandlersWithChildAndParentContextConfiguration.class); + context.refresh(); + SimpleUrlHandlerMapping resourceHandlerMapping = context.getBean("resourceHandlerMapping", + SimpleUrlHandlerMapping.class); + DispatcherServlet extraDispatcherServlet = context.getBean("extraDispatcherServlet", + DispatcherServlet.class); + SimpleUrlHandlerMapping extraResourceHandlerMapping = extraDispatcherServlet.getWebApplicationContext() + .getBean("resourceHandlerMapping", SimpleUrlHandlerMapping.class); + assertThat(resourceHandlerMapping).isNotSameAs(extraResourceHandlerMapping); + assertThat(resourceHandlerMapping.getUrlMap()).containsKey("/**"); + assertThat(extraResourceHandlerMapping.getUrlMap()).containsKey("/**"); + } + } + private void assertCacheControl(AssertableWebApplicationContext context) { Map handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class)); assertThat(handlerMap).hasSize(2); @@ -1330,4 +1354,49 @@ class WebMvcAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ResourceHandlersWithChildAndParentContextConfiguration { + + @Bean + TomcatServletWebServerFactory webServerFactory() { + return new TomcatServletWebServerFactory(0); + } + + @Bean + ServletRegistrationBean additionalDispatcherServlet(DispatcherServlet extraDispatcherServlet) { + ServletRegistrationBean registration = new ServletRegistrationBean<>(extraDispatcherServlet, "/extra/*"); + registration.setName("additionalDispatcherServlet"); + registration.setLoadOnStartup(1); + return registration; + } + + @Bean + private DispatcherServlet extraDispatcherServlet() throws ServletException { + DispatcherServlet dispatcherServlet = new DispatcherServlet(); + AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); + applicationContext.register(ResourceHandlersWithChildAndParentContextChildConfiguration.class); + dispatcherServlet.setApplicationContext(applicationContext); + return dispatcherServlet; + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebMvc + static class ResourceHandlersWithChildAndParentContextChildConfiguration { + + @Bean + WebMvcConfigurer myConfigurer() { + return new WebMvcConfigurer() { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/testtesttest"); + } + + }; + } + + } + }