Skip to content

Commit

Permalink
Add qualified injection points for MVC and WebFlux infrastructure
Browse files Browse the repository at this point in the history
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 spring-projectsgh-23887
  • Loading branch information
snicoll authored and pull[bot] committed Oct 30, 2019
1 parent 7261f31 commit d289a3b
Show file tree
Hide file tree
Showing 4 changed files with 460 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<ViewResolver> resolvers = registry.getViewResolvers();
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(
resolvers, webFluxContentTypeResolver, webFluxAdapterRegistry);
resolvers, contentTypeResolver, reactiveAdapterRegistry);
handler.setDefaultViews(registry.getDefaultViews());
handler.setOrder(registry.getOrder());
return handler;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> Consumer<AnnotationConfigApplicationContext> registerPrimaryBean(String beanName, Class<T> type) {
return context -> context.registerBean(beanName, type, () -> mock(type), definition -> definition.setPrimary(true));
}

private void load(Consumer<AnnotationConfigApplicationContext> context) {
AnnotationConfigApplicationContext testContext = new AnnotationConfigApplicationContext();
context.accept(testContext);
testContext.registerBean(DelegatingWebFluxConfiguration.class);
testContext.refresh();
this.context = testContext;
}
}
Loading

0 comments on commit d289a3b

Please sign in to comment.