diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java new file mode 100644 index 0000000000..d068a457d2 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java @@ -0,0 +1,65 @@ +/* + * 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.web.reactive.function.client; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link WebClient}. + *

This will produce a {@link WebClient.Builder} bean with the {@code prototype} scope, + * meaning each injection point will receive a newly cloned instance of the builder. + * + * @author Brian Clozel + * @since 2.0.0 + */ +@Configuration +@ConditionalOnClass(WebClient.class) +public class WebClientAutoConfiguration { + + private final WebClient.Builder webClientBuilder; + + + public WebClientAutoConfiguration(ObjectProvider> customizerProvider) { + this.webClientBuilder = WebClient.builder(); + List customizers = customizerProvider.getIfAvailable(); + if (!CollectionUtils.isEmpty(customizers)) { + customizers = new ArrayList<>(customizers); + AnnotationAwareOrderComparator.sort(customizers); + customizers.forEach(customizer -> customizer.customize(this.webClientBuilder)); + } + } + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + public WebClient.Builder webClientBuilder(List customizers) { + return this.webClientBuilder.clone(); + } +} 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 81a43a9aa2..03559d5683 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -114,6 +114,7 @@ org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration, org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\ +org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index c2cd673387..35be60e0db 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -105,9 +105,9 @@ public class WebFluxAutoConfigurationTests { .getBean(RequestMappingHandlerAdapter.class); assertThat((List) ReflectionTestUtils .getField(adapter.getArgumentResolverConfigurer(), "customResolvers")) - .contains( - this.context.getBean("firstResolver", - HandlerMethodArgumentResolver.class), + .contains( + this.context.getBean("firstResolver", + HandlerMethodArgumentResolver.class), this.context.getBean("secondResolver", HandlerMethodArgumentResolver.class)); } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfigurationTests.java new file mode 100644 index 0000000000..3c98e035cc --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfigurationTests.java @@ -0,0 +1,131 @@ +/* + * 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.web.reactive.function.client; + +import java.net.URI; + +import org.junit.After; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link WebClientAutoConfiguration} + * + * @author Brian Clozel + */ +public class WebClientAutoConfigurationTests { + + private AnnotationConfigApplicationContext context; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void webClientShouldApplyCustomizers() throws Exception { + load(WebClientCustomizerConfig.class); + WebClient.Builder builder = this.context.getBean(WebClient.Builder.class); + WebClientCustomizer customizer = this.context.getBean(WebClientCustomizer.class); + builder.build(); + verify(customizer).customize(any(WebClient.Builder.class)); + } + + @Test + public void shouldGetPrototypeScopedBean() throws Exception { + load(WebClientCustomizerConfig.class); + + ClientHttpConnector firstConnector = mock(ClientHttpConnector.class); + given(firstConnector.connect(any(), any(), any())).willReturn(Mono.empty()); + WebClient.Builder firstBuilder = this.context.getBean(WebClient.Builder.class); + firstBuilder.clientConnector(firstConnector).baseUrl("http://first.example.org"); + + ClientHttpConnector secondConnector = mock(ClientHttpConnector.class); + given(secondConnector.connect(any(), any(), any())).willReturn(Mono.empty()); + WebClient.Builder secondBuilder = this.context.getBean(WebClient.Builder.class); + secondBuilder.clientConnector(secondConnector).baseUrl("http://second.example.org"); + + assertThat(firstBuilder).isNotEqualTo(secondBuilder); + + firstBuilder.build().get().uri("/foo").exchange().block(); + secondBuilder.build().get().uri("/foo").exchange().block(); + + verify(firstConnector).connect(eq(HttpMethod.GET), eq(URI.create("http://first.example.org/foo")), any()); + verify(secondConnector).connect(eq(HttpMethod.GET), eq(URI.create("http://second.example.org/foo")), any()); + WebClientCustomizer customizer = this.context.getBean(WebClientCustomizer.class); + verify(customizer, times(1)).customize(any(WebClient.Builder.class)); + } + + @Test + public void shouldNotCreateClientBuilderIfAlreadyPresent() throws Exception { + load(WebClientCustomizerConfig.class, CustomWebClientBuilderConfig.class); + WebClient.Builder builder = this.context.getBean(WebClient.Builder.class); + assertThat(builder).isInstanceOf(MyWebClientBuilder.class); + } + + + private void load(Class... config) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(config); + ctx.register(WebClientAutoConfiguration.class); + ctx.refresh(); + this.context = ctx; + } + + @Configuration + static class WebClientCustomizerConfig { + + @Bean + public WebClientCustomizer webClientCustomizer() { + return mock(WebClientCustomizer.class); + } + + } + + @Configuration + static class CustomWebClientBuilderConfig { + + @Bean + public MyWebClientBuilder myWebClientBuilder() { + return mock(MyWebClientBuilder.class); + } + + } + + interface MyWebClientBuilder extends WebClient.Builder { + + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories index 08255a6c2c..9edc8e10b6 100644 --- a/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories @@ -110,7 +110,8 @@ org.springframework.boot.test.autoconfigure.web.client.WebClientRestTemplateAuto org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\ org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\ org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\ -org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration +org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\ +org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration # AutoConfigureWebMvc auto-configuration imports org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc=\ diff --git a/spring-boot/src/main/java/org/springframework/boot/web/reactive/function/client/WebClientCustomizer.java b/spring-boot/src/main/java/org/springframework/boot/web/reactive/function/client/WebClientCustomizer.java new file mode 100644 index 0000000000..3c4e90cd5c --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/web/reactive/function/client/WebClientCustomizer.java @@ -0,0 +1,35 @@ +/* + * 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.web.reactive.function.client; + +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Callback interface that can be used to customize a {@link WebClient.Builder}. + * + * @author Brian Clozel + * @since 2.0.0 + */ +@FunctionalInterface +public interface WebClientCustomizer { + + /** + * Callback to customize a {@link WebClient.Builder} instance. + * @param webClientBuilder the client builder to customize + */ + void customize(WebClient.Builder webClientBuilder); +}