diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index d49b5d6c..90baef23 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -393,6 +393,36 @@ The Ribbon client is used to create a full physical address. See {githubroot}/spring-cloud-netflix/blob/master/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java[RibbonAutoConfiguration] for details of how the `RestTemplate` is set up. +=== Spring WebClient as a Load Balancer Client + +`WebClient` can be automatically configured to use the `LoadBalancerClient`. To create a load balanced `WebClient` create a `WebClient.Builder` `@Bean` and use the `@LoadBalanced` qualifier. + +[source,java,indent=0] +---- +@Configuration +public class MyConfiguration { + + @Bean + @LoadBalanced + public WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } +} + +public class MyClass { + @Autowired + private WebClient.Builder webClientBuilder; + + public Mono doOtherStuff() { + return webClientBuilder.build().get().uri("http://stores/stores") + .retrieve().bodyToMono(String.class); + } +} +---- + +The URI needs to use a virtual host name (ie. service name, not a host name). +The Ribbon client is used to create a full physical address. + ==== Retrying Failed Requests A load balanced `RestTemplate` can be configured to retry failed requests. diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java index 67530c4b..6311a66b 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java @@ -16,10 +16,7 @@ package org.springframework.cloud.client.loadbalancer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -33,6 +30,10 @@ import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.RestTemplate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Auto configuration for Ribbon (client side load balancing). * @@ -51,18 +52,15 @@ public class LoadBalancerAutoConfiguration { private List restTemplates = Collections.emptyList(); @Bean - public SmartInitializingSingleton loadBalancedRestTemplateInitializer( - final List customizers) { - return new SmartInitializingSingleton() { - @Override - public void afterSingletonsInstantiated() { - for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { - for (RestTemplateCustomizer customizer : customizers) { - customizer.customize(restTemplate); - } - } - } - }; + public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated( + final ObjectProvider> restTemplateCustomizers) { + return () -> restTemplateCustomizers.ifAvailable(customizers -> { + for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { + for (RestTemplateCustomizer customizer : customizers) { + customizer.customize(restTemplate); + } + } + }); } @Autowired(required = false) @@ -89,15 +87,12 @@ public class LoadBalancerAutoConfiguration { @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { - return new RestTemplateCustomizer() { - @Override - public void customize(RestTemplate restTemplate) { - List list = new ArrayList<>( - restTemplate.getInterceptors()); - list.add(loadBalancerInterceptor); - restTemplate.setInterceptors(list); - } - }; + return restTemplate -> { + List list = new ArrayList<>( + restTemplate.getInterceptors()); + list.add(loadBalancerInterceptor); + restTemplate.setInterceptors(list); + }; } } @@ -143,15 +138,12 @@ public class LoadBalancerAutoConfiguration { @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final RetryLoadBalancerInterceptor loadBalancerInterceptor) { - return new RestTemplateCustomizer() { - @Override - public void customize(RestTemplate restTemplate) { - List list = new ArrayList<>( - restTemplate.getInterceptors()); - list.add(loadBalancerInterceptor); - restTemplate.setInterceptors(list); - } - }; + return restTemplate -> { + List list = new ArrayList<>( + restTemplate.getInterceptors()); + list.add(loadBalancerInterceptor); + restTemplate.setInterceptors(list); + }; } } } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancerAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancerAutoConfiguration.java index e63a9635..44fd99c8 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancerAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancerAutoConfiguration.java @@ -1,12 +1,19 @@ package org.springframework.cloud.client.loadbalancer.reactive; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; +import java.util.Collections; +import java.util.List; + /** * @author Spencer Gibb */ @@ -15,6 +22,31 @@ import org.springframework.web.reactive.function.client.WebClient; @ConditionalOnBean(LoadBalancerClient.class) public class ReactiveLoadBalancerAutoConfiguration { + @LoadBalanced + @Autowired(required = false) + private List webClientBuilders = Collections.emptyList(); + + public List getBuilders() { + return webClientBuilders; + } + + @Bean + public SmartInitializingSingleton loadBalancedWebClientInitializer( + final List customizers) { + return () -> { + for (WebClient.Builder webClientBuilder : getBuilders()) { + for (WebClientCustomizer customizer : customizers) { + customizer.customize(webClientBuilder); + } + } + }; + } + + @Bean + public WebClientCustomizer loadbalanceClientWebClientCustomizer(LoadBalancerExchangeFilterFunction filterFunction) { + return builder -> builder.filter(filterFunction); + } + @Bean public LoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction(LoadBalancerClient client) { return new LoadBalancerExchangeFilterFunction(client); diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancerAutoConfigurationTests.java new file mode 100644 index 00000000..6a56cd1d --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancerAutoConfigurationTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 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.cloud.client.loadbalancer.reactive; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * @author Spencer Gibb + */ +public class ReactiveLoadBalancerAutoConfigurationTests { + + @Test + public void webClientBuilderGetsLoadBalancerInterceptor() { + ConfigurableApplicationContext context = init(OneWebClientBuilder.class); + final Map webClientBuilders = context + .getBeansOfType(WebClient.Builder.class); + + assertThat(webClientBuilders).isNotNull().hasSize(1); + WebClient.Builder webClientBuilder = webClientBuilders.values().iterator().next(); + assertThat(webClientBuilder).isNotNull(); + + assertLoadBalanced(webClientBuilder); + } + + private void assertLoadBalanced(WebClient.Builder webClientBuilder) { + List filters = getFilters(webClientBuilder); + assertThat(filters).hasSize(1); + ExchangeFilterFunction interceptor = filters.get(0); + assertThat(interceptor).isInstanceOf(LoadBalancerExchangeFilterFunction.class); + } + + @SuppressWarnings("unchecked") + private List getFilters(WebClient.Builder builder) { + return (List) ReflectionTestUtils.getField(builder, "filters"); + } + + @Test + public void multipleWebClientBuilders() { + ConfigurableApplicationContext context = init(TwoWebClientBuilders.class); + final Map webClientBuilders = context + .getBeansOfType(WebClient.Builder.class); + + assertThat(webClientBuilders).hasSize(2); + + TwoWebClientBuilders.Two two = context.getBean(TwoWebClientBuilders.Two.class); + + assertThat(two.loadBalanced).isNotNull(); + assertLoadBalanced(two.loadBalanced); + + assertThat(two.nonLoadBalanced).isNotNull(); + assertThat(getFilters(two.nonLoadBalanced)).isNullOrEmpty(); + } + + protected ConfigurableApplicationContext init(Class config) { + return new SpringApplicationBuilder().web(WebApplicationType.NONE) + // .properties("spring.aop.proxyTargetClass=true") + .sources(config, ReactiveLoadBalancerAutoConfiguration.class).run(); + } + + @Configuration + protected static class OneWebClientBuilder { + + @Bean + @LoadBalanced + WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } + + @Bean + LoadBalancerClient loadBalancerClient() { + return new NoopLoadBalancerClient(); + } + + @Bean + LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() { return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();} + + } + + @Configuration + protected static class TwoWebClientBuilders { + + @Primary + @Bean + WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } + + @LoadBalanced + @Bean + WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } + + @Bean + LoadBalancerClient loadBalancerClient() { + return new NoopLoadBalancerClient(); + } + + @Configuration + protected static class Two { + @Autowired + WebClient.Builder nonLoadBalanced; + + @Autowired + @LoadBalanced + WebClient.Builder loadBalanced; + } + + } + + private static class NoopLoadBalancerClient implements LoadBalancerClient { + private final Random random = new Random(); + + @Override + public ServiceInstance choose(String serviceId) { + return new DefaultServiceInstance(serviceId, serviceId, + this.random.nextInt(40000), false); + } + + @Override + public T execute(String serviceId, LoadBalancerRequest request) { + try { + return request.apply(choose(serviceId)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException { + try { + return request.apply(choose(serviceId)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public URI reconstructURI(ServiceInstance instance, URI original) { + return DefaultServiceInstance.getUri(instance); + } + } +}