diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index f7d6c3bc..c5fbb357 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -275,4 +275,54 @@ For a Spring Boot Actuator application there are some additional management endp == Spring Cloud Commons: Common Abstractions -Patterns such as service discovery, load balancing and circuit breakers lend themselves to a common abstraction layer that can be consumed by all Spring Cloud clients, independent of the implementation (e.g. discovery via Eureka or Consul). \ No newline at end of file +Patterns such as service discovery, load balancing and circuit breakers lend themselves to a common abstraction layer that can be consumed by all Spring Cloud clients, independent of the implementation (e.g. discovery via Eureka or Consul). + + +=== Spring RestTemplate as a Load Balancer Client + +You can use Ribbon indirectly via an autoconfigured `RestTemplate` +when RestTemplate is on the classpath and a `LoadBalancerClient` bean is defined): + +[source,java,indent=0] +---- +public class MyClass { + @Autowired + private RestTemplate restTemplate; + + public String doOtherStuff() { + String results = restTemplate.getForObject("http://stores/stores", String.class); + return results; + } +} +---- + +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. See +{github-code}/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java[RibbonAutoConfiguration] +for details of how the `RestTemplate` is set up. + +=== Multiple RestTemplate objects + +If you want a `RestTemplate` that is not load balanced, create a `RestTemplate` +bean and inject it as normal. To access the load balanced `RestTemplate use +the provided `@LoadBalanced` `Qualifier`: + +[source,java,indent=0] +---- +public class MyClass { + @Autowired + private RestTemplate restTemplate; + + @Autowired + @LoadBalanced + private RestTemplate loadBalanced; + + public String doOtherStuff() { + return loadBalanced.getForObject("http://stores/stores", String.class); + } + + public String doStuff() { + return restTemplate.getForObject("http://example.com", String.class); + } +} +---- \ No newline at end of file diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalanced.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalanced.java new file mode 100644 index 00000000..eaa28727 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalanced.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2015 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; + +import org.springframework.beans.factory.annotation.Qualifier; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient + * @author Spencer Gibb + */ +@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Qualifier +public @interface LoadBalanced { +} 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 157b9605..48aa4ecb 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 @@ -21,7 +21,6 @@ import java.util.List; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestInterceptor; @@ -39,8 +38,8 @@ import org.springframework.web.client.RestTemplate; public class LoadBalancerAutoConfiguration { @Bean - @ConditionalOnMissingBean(RestTemplate.class) - public RestTemplate restTemplate(LoadBalancerInterceptor loadBalancerInterceptor) { + @LoadBalanced + public RestTemplate loadBalancedRestTemplate(LoadBalancerInterceptor loadBalancerInterceptor) { RestTemplate restTemplate = new RestTemplate(); List list = new ArrayList<>(); list.add(loadBalancerInterceptor); diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java new file mode 100644 index 00000000..f672d9bc --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java @@ -0,0 +1,133 @@ +package org.springframework.cloud.client.loadbalancer; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import lombok.SneakyThrows; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +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.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; + +/** + * @author Spencer Gibb + */ +public class LoadBalancerAutoConfigurationTests { + + @Test + public void restTemplateGetsLoadBalancerInterceptor() { + ConfigurableApplicationContext context = init(OneRestTemplate.class); + final Map restTemplates = context.getBeansOfType(RestTemplate.class); + + assertThat(restTemplates, is(notNullValue())); + assertThat(restTemplates.values(), hasSize(1)); + RestTemplate restTemplate = restTemplates.values().iterator().next(); + assertThat(restTemplate, is(notNullValue())); + + assertLoadBalanced(restTemplate); + } + + protected void assertLoadBalanced(RestTemplate restTemplate) { + List interceptors = restTemplate.getInterceptors(); + assertThat(interceptors, hasSize(1)); + ClientHttpRequestInterceptor interceptor = interceptors.get(0); + assertThat(interceptor, is(instanceOf(LoadBalancerInterceptor.class))); + } + + @Test + public void multipleRestTemplates() { + ConfigurableApplicationContext context = init(TwoRestTemplates.class); + final Map restTemplates = context.getBeansOfType(RestTemplate.class); + + assertThat(restTemplates, is(notNullValue())); + Collection templates = restTemplates.values(); + assertThat(templates, hasSize(2)); + + TwoRestTemplates.Two two = context.getBean(TwoRestTemplates.Two.class); + + assertThat(two.loadBalanced, is(notNullValue())); + assertLoadBalanced(two.loadBalanced); + + assertThat(two.nonLoadBalanced, is(notNullValue())); + assertThat(two.nonLoadBalanced.getInterceptors(), is(empty())); + } + + + protected ConfigurableApplicationContext init(Class config) { + return new SpringApplicationBuilder().web(false).sources(config, LoadBalancerAutoConfiguration.class).run(); + } + + @Configuration + protected static class OneRestTemplate { + + @Bean + LoadBalancerClient loadBalancerClient() { + return new NoopLoadBalancerClient(); + } + + } + + @Configuration + protected static class TwoRestTemplates { + + @Primary + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + LoadBalancerClient loadBalancerClient() { + return new NoopLoadBalancerClient(); + } + + @Configuration + protected static class Two { + @Autowired + RestTemplate nonLoadBalanced; + + @Autowired + @LoadBalanced + RestTemplate loadBalanced; + } + + } + + private static class NoopLoadBalancerClient implements LoadBalancerClient { + private final Random random = new Random(); + + @Override + public ServiceInstance choose(String serviceId) { + return new DefaultServiceInstance(serviceId, serviceId, random.nextInt(40000), false); + } + + @Override + @SneakyThrows + public T execute(String serviceId, LoadBalancerRequest request) { + return request.apply(choose(serviceId)); + } + + @Override + public URI reconstructURI(ServiceInstance instance, URI original) { + return DefaultServiceInstance.getUri(instance); + } + } +}