From 8317977e1bf7430c95c9b42b39006f35b6454960 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 22 Feb 2017 17:17:14 +0100 Subject: [PATCH] Add WebFlux auto-configuration This commit creates auto-configuration classes for both the annotation and functional variants of the WebFlux framework. They provide the basic support to get started with those, by creating the required `HttpHandler` using the provided application context (for annotation) or `RouterFunction`s (for functional). They do support `WebFilter` registration and a few advanced features such as resource handling, `messageReaders|Writers` and `ViewResolver` auto-registration. Closes gh-8386 --- spring-boot-autoconfigure/pom.xml | 5 + .../WebFluxAnnotationAutoConfiguration.java | 263 ++++++++++++++++++ .../WebFluxFunctionalAutoConfiguration.java | 113 ++++++++ .../webflux/WebFluxProperties.java | 42 +++ .../main/resources/META-INF/spring.factories | 2 + ...bFluxAnnotationAutoConfigurationTests.java | 257 +++++++++++++++++ ...bFluxFunctionalAutoConfigurationTests.java | 153 ++++++++++ 7 files changed, 835 insertions(+) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxAnnotationAutoConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxFunctionalAutoConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxProperties.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/WebFluxAnnotationAutoConfigurationTests.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/WebFluxFunctionalAutoConfigurationTests.java diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index ddac4abdca..a3be4d7b90 100755 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -354,6 +354,11 @@ spring-websocket true + + org.springframework + spring-webflux + true + org.springframework spring-webmvc diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxAnnotationAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxAnnotationAutoConfiguration.java new file mode 100644 index 0000000000..e669176a87 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxAnnotationAutoConfiguration.java @@ -0,0 +1,263 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.boot.autoconfigure.webflux; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; +import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.format.Formatter; +import org.springframework.format.FormatterRegistry; +import org.springframework.http.CacheControl; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.config.ResourceChainRegistration; +import org.springframework.web.reactive.config.ResourceHandlerRegistration; +import org.springframework.web.reactive.config.ResourceHandlerRegistry; +import org.springframework.web.reactive.config.ViewResolverRegistry; +import org.springframework.web.reactive.config.WebFluxConfigurationSupport; +import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.resource.AppCacheManifestTransformer; +import org.springframework.web.reactive.resource.GzipResourceResolver; +import org.springframework.web.reactive.resource.ResourceResolver; +import org.springframework.web.reactive.resource.VersionResourceResolver; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link EnableWebFlux WebFlux}. + * + * @author Brian Clozel + * @author Rob Winch + */ +@Configuration +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@ConditionalOnClass({DispatcherHandler.class, HttpHandler.class}) +@ConditionalOnMissingBean({RouterFunction.class, HttpHandler.class}) +@AutoConfigureAfter(ReactiveWebServerAutoConfiguration.class) +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) +public class WebFluxAnnotationAutoConfiguration { + + @Configuration + @ConditionalOnMissingBean(WebFluxConfigurationSupport.class) + @EnableConfigurationProperties({ResourceProperties.class, WebFluxProperties.class}) + @Import(DelegatingWebFluxConfiguration.class) + public static class WebFluxConfig implements WebFluxConfigurer { + + private static final Log logger = LogFactory.getLog(WebFluxConfig.class); + + private final ResourceProperties resourceProperties; + + private final WebFluxProperties webFluxProperties; + + private final ListableBeanFactory beanFactory; + + private final List argumentResolvers; + + private final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; + + private final List viewResolvers; + + + public WebFluxConfig(ResourceProperties resourceProperties, + WebFluxProperties webFluxProperties, ListableBeanFactory beanFactory, + ObjectProvider> resolvers, + ObjectProvider resourceHandlerRegistrationCustomizer, + ObjectProvider> viewResolvers) { + this.resourceProperties = resourceProperties; + this.webFluxProperties = webFluxProperties; + this.beanFactory = beanFactory; + this.argumentResolvers = resolvers.getIfAvailable(); + this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizer.getIfAvailable(); + this.viewResolvers = viewResolvers.getIfAvailable(); + } + + @Override + public void addArgumentResolvers(List resolvers) { + if (this.argumentResolvers != null) { + resolvers.addAll(this.argumentResolvers); + } + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + if (!this.resourceProperties.isAddMappings()) { + logger.debug("Default resource handling disabled"); + return; + } + Integer cachePeriod = this.resourceProperties.getCachePeriod(); + if (!registry.hasMappingForPattern("/webjars/**")) { + ResourceHandlerRegistration registration = registry + .addResourceHandler("/webjars/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/"); + if (cachePeriod != null) { + registration.setCacheControl(CacheControl.maxAge(cachePeriod, TimeUnit.SECONDS)); + } + customizeResourceHandlerRegistration(registration); + } + String staticPathPattern = this.webFluxProperties.getStaticPathPattern(); + if (!registry.hasMappingForPattern(staticPathPattern)) { + ResourceHandlerRegistration registration = registry.addResourceHandler(staticPathPattern) + .addResourceLocations(this.resourceProperties.getStaticLocations()); + if (cachePeriod != null) { + registration.setCacheControl(CacheControl.maxAge(cachePeriod, TimeUnit.SECONDS)); + } + customizeResourceHandlerRegistration(registration); + } + } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + if (this.viewResolvers != null) { + AnnotationAwareOrderComparator.sort(this.viewResolvers); + this.viewResolvers.forEach(resolver -> registry.viewResolver(resolver)); + } + } + + @Override + public void addFormatters(final FormatterRegistry registry) { + for (Converter converter : getBeansOfType(Converter.class)) { + registry.addConverter(converter); + } + for (GenericConverter converter : getBeansOfType(GenericConverter.class)) { + registry.addConverter(converter); + } + for (Formatter formatter : getBeansOfType(Formatter.class)) { + registry.addFormatter(formatter); + } + } + + private Collection getBeansOfType(Class type) { + return this.beanFactory.getBeansOfType(type).values(); + } + + private void customizeResourceHandlerRegistration( + ResourceHandlerRegistration registration) { + if (this.resourceHandlerRegistrationCustomizer != null) { + this.resourceHandlerRegistrationCustomizer.customize(registration); + } + + } + } + + @Configuration + @Import(WebFluxConfig.class) + public static class WebHttpHandlerConfiguration implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + } + + @Bean + public HttpHandler httpHandler() { + return WebHttpHandlerBuilder.applicationContext(this.applicationContext).build(); + } + } + + @Configuration + @ConditionalOnEnabledResourceChain + static class ResourceChainCustomizerConfiguration { + + @Bean + public ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() { + return new ResourceChainResourceHandlerRegistrationCustomizer(); + } + } + + interface ResourceHandlerRegistrationCustomizer { + + void customize(ResourceHandlerRegistration registration); + + } + + private static class ResourceChainResourceHandlerRegistrationCustomizer + implements ResourceHandlerRegistrationCustomizer { + + @Autowired + private ResourceProperties resourceProperties = new ResourceProperties(); + + @Override + public void customize(ResourceHandlerRegistration registration) { + ResourceProperties.Chain properties = this.resourceProperties.getChain(); + configureResourceChain(properties, + registration.resourceChain(properties.isCache())); + } + + private void configureResourceChain(ResourceProperties.Chain properties, + ResourceChainRegistration chain) { + ResourceProperties.Strategy strategy = properties.getStrategy(); + if (strategy.getFixed().isEnabled() || strategy.getContent().isEnabled()) { + chain.addResolver(getVersionResourceResolver(strategy)); + } + if (properties.isGzipped()) { + chain.addResolver(new GzipResourceResolver()); + } + if (properties.isHtmlApplicationCache()) { + chain.addTransformer(new AppCacheManifestTransformer()); + } + } + + private ResourceResolver getVersionResourceResolver( + ResourceProperties.Strategy properties) { + VersionResourceResolver resolver = new VersionResourceResolver(); + if (properties.getFixed().isEnabled()) { + String version = properties.getFixed().getVersion(); + String[] paths = properties.getFixed().getPaths(); + resolver.addFixedVersionStrategy(version, paths); + } + if (properties.getContent().isEnabled()) { + String[] paths = properties.getContent().getPaths(); + resolver.addContentVersionStrategy(paths); + } + return resolver; + } + + } +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxFunctionalAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxFunctionalAutoConfiguration.java new file mode 100644 index 0000000000..5fa8df8080 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxFunctionalAutoConfiguration.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.boot.autoconfigure.webflux; + +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.function.server.HandlerStrategies; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.server.session.WebSessionManager; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Functional WebFlux. + * + * @author Brian Clozel + */ +@Configuration +@ConditionalOnClass({DispatcherHandler.class, HttpHandler.class}) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@ConditionalOnBean(RouterFunction.class) +@ConditionalOnMissingBean(HttpHandler.class) +@AutoConfigureAfter({ReactiveWebServerAutoConfiguration.class}) +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) +public class WebFluxFunctionalAutoConfiguration { + + @Configuration + public static class WebFluxFunctionalConfig { + + private final List webFilters; + + private final WebSessionManager webSessionManager; + + private final List messageReaders; + + private final List messageWriters; + + private final List viewResolvers; + + public WebFluxFunctionalConfig(ObjectProvider> webFilters, + ObjectProvider webSessionManager, + ObjectProvider> messageReaders, + ObjectProvider> messageWriters, + ObjectProvider> viewResolvers) { + this.webFilters = webFilters.getIfAvailable(); + if (this.webFilters != null) { + AnnotationAwareOrderComparator.sort(this.webFilters); + } + this.webSessionManager = webSessionManager.getIfAvailable(); + this.messageReaders = messageReaders.getIfAvailable(); + this.messageWriters = messageWriters.getIfAvailable(); + this.viewResolvers = viewResolvers.getIfAvailable(); + } + + @Bean + public HttpHandler httpHandler(List routerFunctions) { + Collections.sort(routerFunctions, new AnnotationAwareOrderComparator()); + RouterFunction routerFunction = routerFunctions.stream().reduce(RouterFunction::and).get(); + HandlerStrategies.Builder strategiesBuilder = HandlerStrategies.builder(); + if (this.messageReaders != null) { + this.messageReaders.forEach(reader -> strategiesBuilder.messageReader(reader)); + } + if (this.messageWriters != null) { + this.messageWriters.forEach(writer -> strategiesBuilder.messageWriter(writer)); + } + if (this.viewResolvers != null) { + this.viewResolvers.forEach(viewResolver -> strategiesBuilder.viewResolver(viewResolver)); + } + WebHandler webHandler = RouterFunctions.toHttpHandler(routerFunction, strategiesBuilder.build()); + WebHttpHandlerBuilder builder = WebHttpHandlerBuilder + .webHandler(webHandler) + .sessionManager(this.webSessionManager); + if (this.webFilters != null) { + builder.filters(this.webFilters.toArray(new WebFilter[this.webFilters.size()])); + } + return builder.build(); + } + } +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxProperties.java new file mode 100644 index 0000000000..cc39c80b32 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/WebFluxProperties.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.boot.autoconfigure.webflux; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties properties} for Spring WebFlux. + * + * @author Brian Clozel + * @since 2.0.0 + */ +@ConfigurationProperties(prefix = "spring.webflux") +public class WebFluxProperties { + + /** + * Path pattern used for static resources. + */ + private String staticPathPattern = "/**"; + + public String getStaticPathPattern() { + return this.staticPathPattern; + } + + public void setStaticPathPattern(String staticPathPattern) { + this.staticPathPattern = staticPathPattern; + } +} diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 2dcde3f4fc..0bcb57b33b 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -113,6 +113,8 @@ org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\ org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration,\ org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\ org.springframework.boot.autoconfigure.webflux.ReactiveWebServerAutoConfiguration,\ +org.springframework.boot.autoconfigure.webflux.WebFluxAnnotationAutoConfiguration,\ +org.springframework.boot.autoconfigure.webflux.WebFluxFunctionalAutoConfiguration,\ org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration,\ org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration,\ org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/WebFluxAnnotationAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/WebFluxAnnotationAutoConfigurationTests.java new file mode 100644 index 0000000000..01394b5da6 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/WebFluxAnnotationAutoConfigurationTests.java @@ -0,0 +1,257 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.boot.autoconfigure.webflux; + +import org.junit.Test; + +import org.springframework.boot.context.embedded.ReactiveWebApplicationContext; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.accept.CompositeContentTypeResolver; +import org.springframework.web.reactive.config.WebFluxConfigurationSupport; +import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; +import org.springframework.web.reactive.resource.CachingResourceResolver; +import org.springframework.web.reactive.resource.CachingResourceTransformer; +import org.springframework.web.reactive.resource.PathResourceResolver; +import org.springframework.web.reactive.resource.ResourceWebHandler; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.adapter.HttpWebHandlerAdapter; +import org.springframework.web.server.handler.FilteringWebHandler; +import org.springframework.web.server.handler.WebHandlerDecorator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link WebFluxAnnotationAutoConfiguration}. + * + * @author Brian Clozel + */ +public class WebFluxAnnotationAutoConfigurationTests { + + private ReactiveWebApplicationContext context; + + @Test + public void shouldNotProcessIfExistingHttpHandler() throws Exception { + load(CustomHttpHandler.class); + + assertThat(this.context.getBeansOfType(RequestMappingHandlerMapping.class).size()).isEqualTo(0); + assertThat(this.context.getBeansOfType(RequestMappingHandlerAdapter.class).size()).isEqualTo(0); + assertThat(this.context.getBeansOfType(HttpWebHandlerAdapter.class).size()).isEqualTo(0); + } + + + @Test + public void shouldNotProcessIfExistingWebReactiveConfiguration() throws Exception { + load(WebFluxConfigurationSupport.class); + + assertThat(this.context.getBeansOfType(RequestMappingHandlerMapping.class).size()).isEqualTo(1); + assertThat(this.context.getBeansOfType(RequestMappingHandlerAdapter.class).size()).isEqualTo(1); + } + + @Test + public void shouldCreateDefaultBeans() throws Exception { + load(BaseConfiguration.class); + + assertThat(this.context.getBeansOfType(RequestMappingHandlerMapping.class).size()).isEqualTo(1); + assertThat(this.context.getBeansOfType(RequestMappingHandlerAdapter.class).size()).isEqualTo(1); + assertThat(this.context.getBeansOfType(CompositeContentTypeResolver.class).size()).isEqualTo(1); + assertThat(this.context.getBean("resourceHandlerMapping", HandlerMapping.class)).isNotNull(); + } + + @Test + public void shouldRegisterCustomHandlerMethodArgumentResolver() throws Exception { + load(CustomArgumentResolvers.class); + + RequestMappingHandlerAdapter adapter = this.context.getBean(RequestMappingHandlerAdapter.class); + assertThat(adapter.getArgumentResolvers()) + .contains(this.context.getBean("firstResolver", HandlerMethodArgumentResolver.class), + this.context.getBean("secondResolver", HandlerMethodArgumentResolver.class)); + } + + @Test + public void shouldRegisterCustomWebFilters() throws Exception { + load(CustomWebFilters.class); + + HttpHandler handler = this.context.getBean(HttpHandler.class); + assertThat(handler).isInstanceOf(WebHandler.class); + WebHandler webHandler = (WebHandler) handler; + while (webHandler instanceof WebHandlerDecorator) { + if (webHandler instanceof FilteringWebHandler) { + FilteringWebHandler filteringWebHandler = (FilteringWebHandler) webHandler; + assertThat(filteringWebHandler.getFilters()).containsExactly( + this.context.getBean("firstWebFilter", WebFilter.class), + this.context.getBean("aWebFilter", WebFilter.class), + this.context.getBean("lastWebFilter", WebFilter.class)); + return; + } + webHandler = ((WebHandlerDecorator) webHandler).getDelegate(); + } + fail("Did not find any FilteringWebHandler"); + } + + @Test + public void shouldRegisterResourceHandlerMapping() throws Exception { + load(BaseConfiguration.class); + + SimpleUrlHandlerMapping hm = this.context.getBean("resourceHandlerMapping", SimpleUrlHandlerMapping.class); + assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class); + ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**"); + assertThat(staticHandler.getLocations()).hasSize(5); + + assertThat(hm.getUrlMap().get("/webjars/**")).isInstanceOf(ResourceWebHandler.class); + ResourceWebHandler webjarsHandler = (ResourceWebHandler) hm.getUrlMap().get("/webjars/**"); + assertThat(webjarsHandler.getLocations()).hasSize(1); + assertThat(webjarsHandler.getLocations().get(0)) + .isEqualTo(new ClassPathResource("/META-INF/resources/webjars/")); + } + + @Test + public void shouldMapResourcesToCustomPath() throws Exception { + load(BaseConfiguration.class, "spring.webflux.static-path-pattern:/static/**"); + SimpleUrlHandlerMapping hm = this.context.getBean("resourceHandlerMapping", SimpleUrlHandlerMapping.class); + assertThat(hm.getUrlMap().get("/static/**")).isInstanceOf(ResourceWebHandler.class); + ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/static/**"); + assertThat(staticHandler.getLocations()).hasSize(5); + } + + @Test + public void shouldNotMapResourcesWhenDisabled() throws Exception { + load(BaseConfiguration.class, "spring.resources.add-mappings:false"); + assertThat(this.context.getBean("resourceHandlerMapping")).isNotInstanceOf(SimpleUrlHandlerMapping.class); + } + + @Test + public void resourceHandlerChainEnabled() throws Exception { + load(BaseConfiguration.class, "spring.resources.chain.enabled:true"); + SimpleUrlHandlerMapping hm = this.context.getBean("resourceHandlerMapping", SimpleUrlHandlerMapping.class); + assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class); + ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**"); + assertThat(staticHandler.getResourceResolvers()).extractingResultOf("getClass") + .containsOnly(CachingResourceResolver.class, PathResourceResolver.class); + assertThat(staticHandler.getResourceTransformers()).extractingResultOf("getClass") + .containsOnly(CachingResourceTransformer.class); + } + + @Test + public void shouldRegisterViewResolvers() throws Exception { + load(ViewResolvers.class); + ViewResolutionResultHandler resultHandler = this.context.getBean(ViewResolutionResultHandler.class); + assertThat(resultHandler.getViewResolvers()).containsExactly( + this.context.getBean("aViewResolver", ViewResolver.class), + this.context.getBean("anotherViewResolver", ViewResolver.class) + ); + } + + private void load(Class config, String... environment) { + this.context = new ReactiveWebApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, environment); + this.context.register(config); + if (!config.equals(BaseConfiguration.class)) { + this.context.register(BaseConfiguration.class); + } + this.context.refresh(); + } + + + @Configuration + protected static class CustomWebFilters { + + @Bean + public WebFilter aWebFilter() { + return mock(WebFilter.class); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public WebFilter lastWebFilter() { + return mock(WebFilter.class); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public WebFilter firstWebFilter() { + return mock(WebFilter.class); + } + } + + @Configuration + protected static class CustomArgumentResolvers { + + @Bean + public HandlerMethodArgumentResolver firstResolver() { + return mock(HandlerMethodArgumentResolver.class); + } + + @Bean + public HandlerMethodArgumentResolver secondResolver() { + return mock(HandlerMethodArgumentResolver.class); + } + + } + + @Configuration + protected static class ViewResolvers { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public ViewResolver aViewResolver() { + return mock(ViewResolver.class); + } + + @Bean + public ViewResolver anotherViewResolver() { + return mock(ViewResolver.class); + } + } + + @Configuration + @Import({WebFluxAnnotationAutoConfiguration.class}) + @EnableConfigurationProperties(WebFluxProperties.class) + protected static class BaseConfiguration { + + @Bean + public MockReactiveWebServerFactory mockReactiveWebServerFactory() { + return new MockReactiveWebServerFactory(); + } + + } + + @Configuration + protected static class CustomHttpHandler { + + @Bean + public HttpHandler httpHandler() { + return (serverHttpRequest, serverHttpResponse) -> null; + } + } +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/WebFluxFunctionalAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/WebFluxFunctionalAutoConfigurationTests.java new file mode 100644 index 0000000000..0099c93fc9 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/WebFluxFunctionalAutoConfigurationTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.boot.autoconfigure.webflux; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.context.embedded.ReactiveWebApplicationContext; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.adapter.HttpWebHandlerAdapter; +import org.springframework.web.server.handler.FilteringWebHandler; +import org.springframework.web.server.handler.WebHandlerDecorator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link WebFluxFunctionalAutoConfiguration}. + * + * @author Brian Clozel + */ +public class WebFluxFunctionalAutoConfigurationTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private ReactiveWebApplicationContext context; + + @Test + public void shouldNotProcessIfExistingHttpHandler() throws Exception { + load(CustomHttpHandler.class); + assertThat(this.context.getBeansOfType(HttpWebHandlerAdapter.class).size()).isEqualTo(0); + } + + @Test + public void shouldFailIfNoHttpHandler() throws Exception { + this.thrown.expect(ApplicationContextException.class); + this.thrown.expectMessage("Unable to start ReactiveWebApplicationContext due to missing HttpHandler bean."); + load(BaseConfiguration.class); + } + + @Test + public void shouldConfigureHttpHandler() { + load(FunctionalConfig.class); + assertThat(this.context.getBeansOfType(HttpHandler.class).size()).isEqualTo(1); + } + + @Test + public void shouldConfigureWebFilters() { + load(FunctionalConfigWithWebFilters.class); + assertThat(this.context.getBeansOfType(HttpHandler.class).size()).isEqualTo(1); + HttpHandler handler = this.context.getBean(HttpHandler.class); + assertThat(handler).isInstanceOf(WebHandler.class); + WebHandler webHandler = (WebHandler) handler; + while (webHandler instanceof WebHandlerDecorator) { + if (webHandler instanceof FilteringWebHandler) { + FilteringWebHandler filteringWebHandler = (FilteringWebHandler) webHandler; + assertThat(filteringWebHandler.getFilters()).containsExactly( + this.context.getBean("customWebFilter", WebFilter.class)); + return; + } + webHandler = ((WebHandlerDecorator) webHandler).getDelegate(); + } + fail("Did not find any FilteringWebHandler"); + } + + + private void load(Class config, String... environment) { + this.context = new ReactiveWebApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, environment); + this.context.register(config); + if (!config.equals(BaseConfiguration.class)) { + this.context.register(BaseConfiguration.class); + } + this.context.refresh(); + } + + + @Configuration + @Import({WebFluxFunctionalAutoConfiguration.class}) + @EnableConfigurationProperties(WebFluxProperties.class) + protected static class BaseConfiguration { + + @Bean + public MockReactiveWebServerFactory mockReactiveWebServerFactory() { + return new MockReactiveWebServerFactory(); + } + } + + @Configuration + protected static class FunctionalConfig { + + @Bean + public RouterFunction routerFunction() { + return RouterFunctions.route(RequestPredicates.GET("/test"), serverRequest -> null); + } + } + + @Configuration + protected static class FunctionalConfigWithWebFilters { + + @Bean + public RouterFunction routerFunction() { + return RouterFunctions.route(RequestPredicates.GET("/test"), serverRequest -> null); + } + + @Bean + public WebFilter customWebFilter() { + return (serverWebExchange, webFilterChain) -> null; + } + } + + @Configuration + protected static class CustomHttpHandler { + + @Bean + public HttpHandler httpHandler() { + return (serverHttpRequest, serverHttpResponse) -> null; + } + + @Bean + public RouterFunction routerFunction() { + return RouterFunctions.route(RequestPredicates.GET("/test"), serverRequest -> null); + } + } + +}