From d289a3b39b5b838d17f42a1dbf16aa47acb31281 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 30 Oct 2019 14:52:43 +0100 Subject: [PATCH] Add qualified injection points for MVC and WebFlux infrastructure Previously, the infrastructure provided by WebMvcConfigurationSupport and WebFluxConfigurationSupport can lead to unexpected results due to the lack of qualifier for certain dependencies. Those configuration classes refer to very specific beans, yet their injection points do not define such qualifiers. As a result, if a candidate exists for the requested type, the context will inject the existing bean and will ignore a most specific one as such constraint it not defined. This can be easily reproduced by having a primary Validator whereas a dedicated "mvcValidator" is expected. Note that a parameter name is in no way a constraint as the name is only used as a fallback when a single candidate cannot be determined. This commit provides explicit @Qualifier metadata for such injection points, renaming the parameter name in the process to clarify that it isn't relevant for the proper bean to be resolved by the context. Closes gh-23887 --- .../config/WebFluxConfigurationSupport.java | 33 +-- ...gWebFluxConfigurationIntegrationTests.java | 199 +++++++++++++++++ .../WebMvcConfigurationSupport.java | 76 ++++--- ...ngWebMvcConfigurationIntegrationTests.java | 201 ++++++++++++++++++ 4 files changed, 460 insertions(+), 49 deletions(-) create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationIntegrationTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationIntegrationTests.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index ab99a8d5d1c5..f2f40e8ea506 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -24,6 +24,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; @@ -120,10 +121,10 @@ public WebExceptionHandler responseStatusExceptionHandler() { @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping( - RequestedContentTypeResolver webFluxContentTypeResolver) { + @Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) { RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping(); mapping.setOrder(0); - mapping.setContentTypeResolver(webFluxContentTypeResolver); + mapping.setContentTypeResolver(contentTypeResolver); mapping.setCorsConfigurations(getCorsConfigurations()); PathMatchConfigurer configurer = getPathMatchConfigurer(); @@ -265,14 +266,14 @@ protected void addResourceHandlers(ResourceHandlerRegistry registry) { @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter( - ReactiveAdapterRegistry webFluxAdapterRegistry, + @Qualifier("webFluxAdapterRegistry") ReactiveAdapterRegistry reactiveAdapterRegistry, ServerCodecConfigurer serverCodecConfigurer, - FormattingConversionService webFluxConversionService, - Validator webfluxValidator) { + @Qualifier("webFluxConversionService") FormattingConversionService conversionService, + @Qualifier("webFluxValidator") Validator validator) { RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); adapter.setMessageReaders(serverCodecConfigurer.getReaders()); - adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(webFluxConversionService, webfluxValidator)); - adapter.setReactiveAdapterRegistry(webFluxAdapterRegistry); + adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator)); + adapter.setReactiveAdapterRegistry(reactiveAdapterRegistry); ArgumentResolverConfigurer configurer = new ArgumentResolverConfigurer(); configureArgumentResolvers(configurer); @@ -426,30 +427,30 @@ public SimpleHandlerAdapter simpleHandlerAdapter() { @Bean public ResponseEntityResultHandler responseEntityResultHandler( - ReactiveAdapterRegistry webFluxAdapterRegistry, + @Qualifier("webFluxAdapterRegistry") ReactiveAdapterRegistry reactiveAdapterRegistry, ServerCodecConfigurer serverCodecConfigurer, - RequestedContentTypeResolver webFluxContentTypeResolver) { + @Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) { return new ResponseEntityResultHandler(serverCodecConfigurer.getWriters(), - webFluxContentTypeResolver, webFluxAdapterRegistry); + contentTypeResolver, reactiveAdapterRegistry); } @Bean public ResponseBodyResultHandler responseBodyResultHandler( - ReactiveAdapterRegistry webFluxAdapterRegistry, + @Qualifier("webFluxAdapterRegistry") ReactiveAdapterRegistry reactiveAdapterRegistry, ServerCodecConfigurer serverCodecConfigurer, - RequestedContentTypeResolver webFluxContentTypeResolver) { + @Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) { return new ResponseBodyResultHandler(serverCodecConfigurer.getWriters(), - webFluxContentTypeResolver, webFluxAdapterRegistry); + contentTypeResolver, reactiveAdapterRegistry); } @Bean public ViewResolutionResultHandler viewResolutionResultHandler( - ReactiveAdapterRegistry webFluxAdapterRegistry, - RequestedContentTypeResolver webFluxContentTypeResolver) { + @Qualifier("webFluxAdapterRegistry") ReactiveAdapterRegistry reactiveAdapterRegistry, + @Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) { ViewResolverRegistry registry = getViewResolverRegistry(); List resolvers = registry.getViewResolvers(); ViewResolutionResultHandler handler = new ViewResolutionResultHandler( - resolvers, webFluxContentTypeResolver, webFluxAdapterRegistry); + resolvers, contentTypeResolver, reactiveAdapterRegistry); handler.setDefaultViews(registry.getDefaultViews()); handler.setOrder(registry.getOrder()); return handler; diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationIntegrationTests.java new file mode 100644 index 000000000000..35df96e88bba --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationIntegrationTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.config; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.validation.Validator; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; +import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler; +import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Integration tests for {@link DelegatingWebFluxConfiguration}. + * + * @author Stephane Nicoll + */ +public class DelegatingWebFluxConfigurationIntegrationTests { + + private AnnotationConfigApplicationContext context; + + @AfterEach + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + void requestMappingHandlerMappingUsesWebFluxInfrastructureByDefault() { + load(context -> { }); + RequestMappingHandlerMapping handlerMapping = this.context.getBean(RequestMappingHandlerMapping.class); + assertThat(handlerMapping.getContentTypeResolver()).isSameAs(this.context.getBean("webFluxContentTypeResolver")); + } + + @Test + void requestMappingHandlerMappingWithPrimaryUsesQualifiedRequestedContentTypeResolver() { + load(registerPrimaryBean("testContentTypeResolver", RequestedContentTypeResolver.class)); + RequestMappingHandlerMapping handlerMapping = this.context.getBean(RequestMappingHandlerMapping.class); + assertThat(handlerMapping.getContentTypeResolver()).isSameAs(this.context.getBean("webFluxContentTypeResolver")); + assertThat(this.context.getBeansOfType(RequestedContentTypeResolver.class)).containsOnlyKeys( + "webFluxContentTypeResolver", "testContentTypeResolver"); + } + + @Test + void requestMappingHandlerAdapterUsesWebFluxInfrastructureByDefault() { + load(context -> { }); + RequestMappingHandlerAdapter mappingHandlerAdapter = this.context.getBean(RequestMappingHandlerAdapter.class); + assertThat(mappingHandlerAdapter.getReactiveAdapterRegistry()).isSameAs(this.context.getBean("webFluxAdapterRegistry")); + assertThat(mappingHandlerAdapter.getWebBindingInitializer()).hasFieldOrPropertyWithValue("conversionService", + this.context.getBean("webFluxConversionService")); + assertThat(mappingHandlerAdapter.getWebBindingInitializer()).hasFieldOrPropertyWithValue("validator", + this.context.getBean("webFluxValidator")); + } + + @Test + void requestMappingHandlerAdapterWithPrimaryUsesQualifiedReactiveAdapterRegistry() { + load(registerPrimaryBean("testReactiveAdapterRegistry", ReactiveAdapterRegistry.class)); + RequestMappingHandlerAdapter mappingHandlerAdapter = this.context.getBean(RequestMappingHandlerAdapter.class); + assertThat(mappingHandlerAdapter.getReactiveAdapterRegistry()).isSameAs(this.context.getBean("webFluxAdapterRegistry")); + assertThat(this.context.getBeansOfType(ReactiveAdapterRegistry.class)).containsOnlyKeys( + "webFluxAdapterRegistry", "testReactiveAdapterRegistry"); + } + + @Test + void requestMappingHandlerAdapterWithPrimaryUsesQualifiedConversionService() { + load(registerPrimaryBean("testConversionService", FormattingConversionService.class)); + RequestMappingHandlerAdapter mappingHandlerAdapter = this.context.getBean(RequestMappingHandlerAdapter.class); + assertThat(mappingHandlerAdapter.getWebBindingInitializer()).hasFieldOrPropertyWithValue("conversionService", + this.context.getBean("webFluxConversionService")); + assertThat(this.context.getBeansOfType(FormattingConversionService.class)).containsOnlyKeys( + "webFluxConversionService", "testConversionService"); + } + + @Test + void requestMappingHandlerAdapterWithPrimaryUsesQualifiedValidator() { + load(registerPrimaryBean("testValidator", Validator.class)); + RequestMappingHandlerAdapter mappingHandlerAdapter = this.context.getBean(RequestMappingHandlerAdapter.class); + assertThat(mappingHandlerAdapter.getWebBindingInitializer()).hasFieldOrPropertyWithValue("validator", + this.context.getBean("webFluxValidator")); + assertThat(this.context.getBeansOfType(Validator.class)).containsOnlyKeys( + "webFluxValidator", "testValidator"); + } + + @Test + void responseEntityResultHandlerUsesWebFluxInfrastructureByDefault() { + load(context -> { }); + ResponseEntityResultHandler responseEntityResultHandler = this.context.getBean(ResponseEntityResultHandler.class); + assertThat(responseEntityResultHandler.getAdapterRegistry()).isSameAs(this.context.getBean("webFluxAdapterRegistry")); + assertThat(responseEntityResultHandler.getContentTypeResolver()).isSameAs(this.context.getBean("webFluxContentTypeResolver")); + } + + @Test + void responseEntityResultHandlerWithPrimaryUsesQualifiedReactiveAdapterRegistry() { + load(registerPrimaryBean("testReactiveAdapterRegistry", ReactiveAdapterRegistry.class)); + ResponseEntityResultHandler responseEntityResultHandler = this.context.getBean(ResponseEntityResultHandler.class); + assertThat(responseEntityResultHandler.getAdapterRegistry()).isSameAs(this.context.getBean("webFluxAdapterRegistry")); + assertThat(this.context.getBeansOfType(ReactiveAdapterRegistry.class)).containsOnlyKeys( + "webFluxAdapterRegistry", "testReactiveAdapterRegistry"); + } + + @Test + void responseEntityResultHandlerWithPrimaryUsesQualifiedRequestedContentTypeResolver() { + load(registerPrimaryBean("testContentTypeResolver", RequestedContentTypeResolver.class)); + ResponseEntityResultHandler responseEntityResultHandler = this.context.getBean(ResponseEntityResultHandler.class); + assertThat(responseEntityResultHandler.getContentTypeResolver()).isSameAs(this.context.getBean("webFluxContentTypeResolver")); + assertThat(this.context.getBeansOfType(RequestedContentTypeResolver.class)).containsOnlyKeys( + "webFluxContentTypeResolver", "testContentTypeResolver"); + } + + @Test + void responseBodyResultHandlerUsesWebFluxInfrastructureByDefault() { + load(context -> { }); + ResponseBodyResultHandler responseBodyResultHandler = this.context.getBean(ResponseBodyResultHandler.class); + assertThat(responseBodyResultHandler.getAdapterRegistry()).isSameAs(this.context.getBean("webFluxAdapterRegistry")); + assertThat(responseBodyResultHandler.getContentTypeResolver()).isSameAs(this.context.getBean("webFluxContentTypeResolver")); + } + + @Test + void responseBodyResultHandlerWithPrimaryUsesQualifiedReactiveAdapterRegistry() { + load(registerPrimaryBean("testReactiveAdapterRegistry", ReactiveAdapterRegistry.class)); + ResponseBodyResultHandler responseBodyResultHandler = this.context.getBean(ResponseBodyResultHandler.class); + assertThat(responseBodyResultHandler.getAdapterRegistry()).isSameAs(this.context.getBean("webFluxAdapterRegistry")); + assertThat(this.context.getBeansOfType(ReactiveAdapterRegistry.class)).containsOnlyKeys( + "webFluxAdapterRegistry", "testReactiveAdapterRegistry"); + } + + @Test + void responseBodyResultHandlerWithPrimaryUsesQualifiedRequestedContentTypeResolver() { + load(registerPrimaryBean("testContentTypeResolver", RequestedContentTypeResolver.class)); + ResponseBodyResultHandler responseBodyResultHandler = this.context.getBean(ResponseBodyResultHandler.class); + assertThat(responseBodyResultHandler.getContentTypeResolver()).isSameAs(this.context.getBean("webFluxContentTypeResolver")); + assertThat(this.context.getBeansOfType(RequestedContentTypeResolver.class)).containsOnlyKeys( + "webFluxContentTypeResolver", "testContentTypeResolver"); + } + + @Test + void viewResolutionResultHandlerUsesWebFluxInfrastructureByDefault() { + load(context -> { }); + ViewResolutionResultHandler viewResolutionResultHandler = this.context.getBean(ViewResolutionResultHandler.class); + assertThat(viewResolutionResultHandler.getAdapterRegistry()).isSameAs(this.context.getBean("webFluxAdapterRegistry")); + assertThat(viewResolutionResultHandler.getContentTypeResolver()).isSameAs(this.context.getBean("webFluxContentTypeResolver")); + } + + @Test + void viewResolutionResultHandlerWithPrimaryUsesQualifiedReactiveAdapterRegistry() { + load(registerPrimaryBean("testReactiveAdapterRegistry", ReactiveAdapterRegistry.class)); + ViewResolutionResultHandler viewResolutionResultHandler = this.context.getBean(ViewResolutionResultHandler.class); + assertThat(viewResolutionResultHandler.getAdapterRegistry()).isSameAs(this.context.getBean("webFluxAdapterRegistry")); + assertThat(this.context.getBeansOfType(ReactiveAdapterRegistry.class)).containsOnlyKeys( + "webFluxAdapterRegistry", "testReactiveAdapterRegistry"); + } + + @Test + void viewResolutionResultHandlerWithPrimaryUsesQualifiedRequestedContentTypeResolver() { + load(registerPrimaryBean("testContentTypeResolver", RequestedContentTypeResolver.class)); + ViewResolutionResultHandler viewResolutionResultHandler = this.context.getBean(ViewResolutionResultHandler.class); + assertThat(viewResolutionResultHandler.getContentTypeResolver()).isSameAs(this.context.getBean("webFluxContentTypeResolver")); + assertThat(this.context.getBeansOfType(RequestedContentTypeResolver.class)).containsOnlyKeys( + "webFluxContentTypeResolver", "testContentTypeResolver"); + } + + private Consumer registerPrimaryBean(String beanName, Class type) { + return context -> context.registerBean(beanName, type, () -> mock(type), definition -> definition.setPrimary(true)); + } + + private void load(Consumer context) { + AnnotationConfigApplicationContext testContext = new AnnotationConfigApplicationContext(); + context.accept(testContext); + testContext.registerBean(DelegatingWebFluxConfiguration.class); + testContext.refresh(); + this.context = testContext; + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index e59868bfba25..d8f93bf4e545 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -29,6 +29,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; @@ -275,13 +276,14 @@ public final ServletContext getServletContext() { */ @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping( - ContentNegotiationManager mvcContentNegotiationManager, - FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { + @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, + @Qualifier("mvcConversionService") FormattingConversionService conversionService, + @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping(); mapping.setOrder(0); - mapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); - mapping.setContentNegotiationManager(mvcContentNegotiationManager); + mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); + mapping.setContentNegotiationManager(contentNegotiationManager); mapping.setCorsConfigurations(getCorsConfigurations()); PathMatchConfigurer configurer = getPathMatchConfigurer(); @@ -447,10 +449,11 @@ protected void configureContentNegotiation(ContentNegotiationConfigurer configur */ @Bean @Nullable - public HandlerMapping viewControllerHandlerMapping(PathMatcher mvcPathMatcher, - UrlPathHelper mvcUrlPathHelper, - FormattingConversionService mvcConversionService, - ResourceUrlProvider mvcResourceUrlProvider) { + public HandlerMapping viewControllerHandlerMapping( + @Qualifier("mvcPathMatcher") PathMatcher pathMatcher, + @Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper, + @Qualifier("mvcConversionService") FormattingConversionService conversionService, + @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext); addViewControllers(registry); @@ -458,9 +461,9 @@ public HandlerMapping viewControllerHandlerMapping(PathMatcher mvcPathMatcher, if (handlerMapping == null) { return null; } - handlerMapping.setPathMatcher(mvcPathMatcher); - handlerMapping.setUrlPathHelper(mvcUrlPathHelper); - handlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); + handlerMapping.setPathMatcher(pathMatcher); + handlerMapping.setUrlPathHelper(urlPathHelper); + handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); handlerMapping.setCorsConfigurations(getCorsConfigurations()); return handlerMapping; } @@ -478,11 +481,12 @@ protected void addViewControllers(ViewControllerRegistry registry) { */ @Bean public BeanNameUrlHandlerMapping beanNameHandlerMapping( - FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { + @Qualifier("mvcConversionService") FormattingConversionService conversionService, + @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping(); mapping.setOrder(2); - mapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); + mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); mapping.setCorsConfigurations(getCorsConfigurations()); return mapping; } @@ -500,11 +504,12 @@ public BeanNameUrlHandlerMapping beanNameHandlerMapping( */ @Bean public RouterFunctionMapping routerFunctionMapping( - FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { + @Qualifier("mvcConversionService") FormattingConversionService conversionService, + @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { RouterFunctionMapping mapping = new RouterFunctionMapping(); mapping.setOrder(3); - mapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); + mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); mapping.setCorsConfigurations(getCorsConfigurations()); mapping.setMessageConverters(getMessageConverters()); return mapping; @@ -517,24 +522,27 @@ public RouterFunctionMapping routerFunctionMapping( */ @Bean @Nullable - public HandlerMapping resourceHandlerMapping(UrlPathHelper mvcUrlPathHelper, - PathMatcher mvcPathMatcher, ContentNegotiationManager mvcContentNegotiationManager, - FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { + 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(this.applicationContext != null, "No ApplicationContext set"); Assert.state(this.servletContext != null, "No ServletContext set"); ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext, - this.servletContext, mvcContentNegotiationManager, mvcUrlPathHelper); + this.servletContext, contentNegotiationManager, urlPathHelper); addResourceHandlers(registry); AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); if (handlerMapping == null) { return null; } - handlerMapping.setPathMatcher(mvcPathMatcher); - handlerMapping.setUrlPathHelper(mvcUrlPathHelper); - handlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); + handlerMapping.setPathMatcher(pathMatcher); + handlerMapping.setUrlPathHelper(urlPathHelper); + handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); handlerMapping.setCorsConfigurations(getCorsConfigurations()); return handlerMapping; } @@ -597,13 +605,14 @@ protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer c */ @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter( - ContentNegotiationManager mvcContentNegotiationManager, - FormattingConversionService mvcConversionService, Validator mvcValidator) { + @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, + @Qualifier("mvcConversionService") FormattingConversionService conversionService, + @Qualifier("mvcValidator") Validator validator) { RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); - adapter.setContentNegotiationManager(mvcContentNegotiationManager); + adapter.setContentNegotiationManager(contentNegotiationManager); adapter.setMessageConverters(getMessageConverters()); - adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(mvcConversionService, mvcValidator)); + adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator)); adapter.setCustomArgumentResolvers(getArgumentResolvers()); adapter.setCustomReturnValueHandlers(getReturnValueHandlers()); @@ -898,10 +907,10 @@ else if (jsonbPresent) { */ @Bean public CompositeUriComponentsContributor mvcUriComponentsContributor( - FormattingConversionService mvcConversionService, - RequestMappingHandlerAdapter requestMappingHandlerAdapter) { + @Qualifier("mvcConversionService") FormattingConversionService conversionService, + @Qualifier("requestMappingHandlerAdapter") RequestMappingHandlerAdapter requestMappingHandlerAdapter) { return new CompositeUriComponentsContributor( - requestMappingHandlerAdapter.getArgumentResolvers(), mvcConversionService); + requestMappingHandlerAdapter.getArgumentResolvers(), conversionService); } /** @@ -932,11 +941,11 @@ public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() { */ @Bean public HandlerExceptionResolver handlerExceptionResolver( - ContentNegotiationManager mvcContentNegotiationManager) { + @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) { List exceptionResolvers = new ArrayList<>(); configureHandlerExceptionResolvers(exceptionResolvers); if (exceptionResolvers.isEmpty()) { - addDefaultHandlerExceptionResolvers(exceptionResolvers, mvcContentNegotiationManager); + addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager); } extendHandlerExceptionResolvers(exceptionResolvers); HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite(); @@ -1026,9 +1035,10 @@ protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResol * @since 4.1 */ @Bean - public ViewResolver mvcViewResolver(ContentNegotiationManager mvcContentNegotiationManager) { + public ViewResolver mvcViewResolver( + @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) { ViewResolverRegistry registry = - new ViewResolverRegistry(mvcContentNegotiationManager, this.applicationContext); + new ViewResolverRegistry(contentNegotiationManager, this.applicationContext); configureViewResolvers(registry); if (registry.getViewResolvers().isEmpty() && this.applicationContext != null) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationIntegrationTests.java new file mode 100644 index 000000000000..f1ba8110e438 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationIntegrationTests.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config.annotation; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionService; +import org.springframework.mock.web.test.MockServletContext; +import org.springframework.util.PathMatcher; +import org.springframework.validation.Validator; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.servlet.handler.AbstractHandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.UrlPathHelper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Integration tests for {@link DelegatingWebMvcConfiguration}. + * + * @author Stephane Nicoll + */ +public class DelegatingWebMvcConfigurationIntegrationTests { + + private ConfigurableApplicationContext context; + + @AfterEach + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + void requestMappingHandlerMappingUsesMvcInfrastructureByDefault() { + load(context -> { }); + RequestMappingHandlerMapping handlerMapping = this.context.getBean(RequestMappingHandlerMapping.class); + assertThat(handlerMapping.getContentNegotiationManager()).isSameAs(this.context.getBean("mvcContentNegotiationManager")); + } + + @Test + void requestMappingHandlerMappingWithPrimaryUsesQualifiedContentNegotiationManager() { + load(registerPrimaryBean("testContentNegotiationManager", ContentNegotiationManager.class)); + RequestMappingHandlerMapping handlerMapping = this.context.getBean(RequestMappingHandlerMapping.class); + assertThat(handlerMapping.getContentNegotiationManager()).isSameAs(this.context.getBean("mvcContentNegotiationManager")); + assertThat(this.context.getBeansOfType(ContentNegotiationManager.class)).containsOnlyKeys( + "mvcContentNegotiationManager", "testContentNegotiationManager"); + } + + @Test + void viewControllerHandlerMappingUsesMvcInfrastructureByDefault() { + load(context -> context.registerBean(ViewControllerConfiguration.class)); + AbstractHandlerMapping handlerMapping = this.context.getBean("viewControllerHandlerMapping", AbstractHandlerMapping.class); + assertThat(handlerMapping.getPathMatcher()).isSameAs(this.context.getBean("mvcPathMatcher")); + assertThat(handlerMapping.getUrlPathHelper()).isSameAs(this.context.getBean("mvcUrlPathHelper")); + } + + @Test + void viewControllerHandlerMappingWithPrimaryUsesQualifiedPathMatcher() { + load(registerPrimaryBean("testPathMatcher", PathMatcher.class) + .andThen(context -> context.registerBean(ViewControllerConfiguration.class))); + AbstractHandlerMapping handlerMapping = this.context.getBean("viewControllerHandlerMapping", AbstractHandlerMapping.class); + assertThat(handlerMapping.getPathMatcher()).isSameAs(this.context.getBean("mvcPathMatcher")); + assertThat(this.context.getBeansOfType(PathMatcher.class)).containsOnlyKeys( + "mvcPathMatcher", "testPathMatcher"); + } + + @Test + void viewControllerHandlerMappingWithPrimaryUsesQualifiedUrlPathHelper() { + load(registerPrimaryBean("testUrlPathHelper", UrlPathHelper.class) + .andThen(context -> context.registerBean(ViewControllerConfiguration.class))); + AbstractHandlerMapping handlerMapping = this.context.getBean("viewControllerHandlerMapping", AbstractHandlerMapping.class); + assertThat(handlerMapping.getUrlPathHelper()).isSameAs(this.context.getBean("mvcUrlPathHelper")); + assertThat(this.context.getBeansOfType(UrlPathHelper.class)).containsOnlyKeys( + "mvcUrlPathHelper", "testUrlPathHelper"); + } + + @Test + void resourceHandlerMappingUsesMvcInfrastructureByDefault() { + load(context -> context.registerBean(ResourceHandlerConfiguration.class)); + AbstractHandlerMapping handlerMapping = this.context.getBean("resourceHandlerMapping", AbstractHandlerMapping.class); + assertThat(handlerMapping.getPathMatcher()).isSameAs(this.context.getBean("mvcPathMatcher")); + assertThat(handlerMapping.getUrlPathHelper()).isSameAs(this.context.getBean("mvcUrlPathHelper")); + } + + @Test + void resourceHandlerMappingWithPrimaryUsesQualifiedPathMatcher() { + load(registerPrimaryBean("testPathMatcher", PathMatcher.class) + .andThen(context -> context.registerBean(ResourceHandlerConfiguration.class))); + AbstractHandlerMapping handlerMapping = this.context.getBean("resourceHandlerMapping", AbstractHandlerMapping.class); + assertThat(handlerMapping.getPathMatcher()).isSameAs(this.context.getBean("mvcPathMatcher")); + assertThat(this.context.getBeansOfType(PathMatcher.class)).containsOnlyKeys( + "mvcPathMatcher", "testPathMatcher"); + } + + @Test + void resourceHandlerMappingWithPrimaryUsesQualifiedUrlPathHelper() { + load(registerPrimaryBean("testUrlPathHelper", UrlPathHelper.class) + .andThen(context -> context.registerBean(ResourceHandlerConfiguration.class))); + AbstractHandlerMapping handlerMapping = this.context.getBean("resourceHandlerMapping", AbstractHandlerMapping.class); + assertThat(handlerMapping.getUrlPathHelper()).isSameAs(this.context.getBean("mvcUrlPathHelper")); + assertThat(this.context.getBeansOfType(UrlPathHelper.class)).containsOnlyKeys( + "mvcUrlPathHelper", "testUrlPathHelper"); + } + + @Test + void requestMappingHandlerAdapterUsesMvcInfrastructureByDefault() { + load(context -> { }); + RequestMappingHandlerAdapter mappingHandlerAdapter = this.context.getBean(RequestMappingHandlerAdapter.class); + assertThat(mappingHandlerAdapter).hasFieldOrPropertyWithValue( + "contentNegotiationManager", this.context.getBean("mvcContentNegotiationManager")); + assertThat(mappingHandlerAdapter.getWebBindingInitializer()).hasFieldOrPropertyWithValue( + "conversionService", this.context.getBean("mvcConversionService")); + assertThat(mappingHandlerAdapter.getWebBindingInitializer()).hasFieldOrPropertyWithValue( + "validator", this.context.getBean("mvcValidator")); + } + + @Test + void requestMappingHandlerAdapterWithPrimaryUsesQualifiedContentNegotiationManager() { + load(registerPrimaryBean("testContentNegotiationManager", ContentNegotiationManager.class)); + RequestMappingHandlerAdapter mappingHandlerAdapter = this.context.getBean(RequestMappingHandlerAdapter.class); + assertThat(mappingHandlerAdapter).hasFieldOrPropertyWithValue( + "contentNegotiationManager", this.context.getBean("mvcContentNegotiationManager")); + assertThat(this.context.getBeansOfType(ContentNegotiationManager.class)).containsOnlyKeys( + "mvcContentNegotiationManager", "testContentNegotiationManager"); + } + + @Test + void requestMappingHandlerAdapterWithPrimaryUsesQualifiedConversionService() { + load(registerPrimaryBean("testConversionService", ConversionService.class)); + RequestMappingHandlerAdapter mappingHandlerAdapter = this.context.getBean(RequestMappingHandlerAdapter.class); + assertThat(mappingHandlerAdapter.getWebBindingInitializer()).hasFieldOrPropertyWithValue( + "conversionService", this.context.getBean("mvcConversionService")); + assertThat(this.context.getBeansOfType(ConversionService.class)).containsOnlyKeys("mvcConversionService", "testConversionService"); + } + + @Test + void requestMappingHandlerAdapterWithPrimaryUsesQualifiedValidator() { + load(registerPrimaryBean("testValidator", Validator.class)); + RequestMappingHandlerAdapter mappingHandlerAdapter = this.context.getBean(RequestMappingHandlerAdapter.class); + assertThat(mappingHandlerAdapter.getWebBindingInitializer()).hasFieldOrPropertyWithValue( + "validator", this.context.getBean("mvcValidator")); + assertThat(this.context.getBeansOfType(Validator.class)).containsOnlyKeys("mvcValidator", "testValidator"); + } + + private Consumer registerPrimaryBean(String beanName, Class type) { + return context -> context.registerBean(beanName, type, () -> mock(type), definition -> definition.setPrimary(true)); + } + + private void load(Consumer context) { + GenericWebApplicationContext webContext = new GenericWebApplicationContext(); + AnnotationConfigUtils.registerAnnotationConfigProcessors(webContext); + webContext.setServletContext(new MockServletContext()); + + context.accept(webContext); + webContext.registerBean(DelegatingWebMvcConfiguration.class); + webContext.refresh(); + this.context = webContext; + } + + @Configuration + static class ViewControllerConfiguration implements WebMvcConfigurer { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/test"); + } + } + + @Configuration + static class ResourceHandlerConfiguration implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**"); + } + } +}