diff --git a/docs/modules/ROOT/pages/spring-cloud-commons/common-abstractions.adoc b/docs/modules/ROOT/pages/spring-cloud-commons/common-abstractions.adoc index 3c5a2012..ad3d743c 100644 --- a/docs/modules/ROOT/pages/spring-cloud-commons/common-abstractions.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-commons/common-abstractions.adoc @@ -135,7 +135,7 @@ Please see the documentation of the `ServiceRegistry` implementation you use for For instance, Eureka's supported statuses are `UP`, `DOWN`, `OUT_OF_SERVICE`, and `UNKNOWN`. [[rest-template-loadbalancer-client]] -== Spring RestTemplate as a Load Balancer Client +== Spring `RestTemplate` as a LoadBalancer Client You can configure a `RestTemplate` to use a Load-balancer client. To create a load-balanced `RestTemplate`, create a `RestTemplate` `@Bean` and use the `@LoadBalanced` qualifier, as the following example shows: @@ -157,8 +157,8 @@ public class MyClass { private RestTemplate restTemplate; public String doOtherStuff() { - String results = restTemplate.getForObject("http://stores/stores", String.class); - return results; + String result = restTemplate.getForObject("http://stores/stores", String.class); + return result; } } ---- @@ -167,13 +167,140 @@ CAUTION: A `RestTemplate` bean is no longer created through auto-configuration. Individual applications must create it. The URI needs to use a virtual host name (that is, a service name, not a host name). -The BlockingLoadBalancerClient is used to create a full physical address. +The `BlockingLoadBalancerClient` is used to create a full physical address. -IMPORTANT: To use a load-balanced `RestTemplate`, you need to have a load-balancer implementation in your classpath. +IMPORTANT: To use a load-balanced `RestTemplate`, you need to have a Spring Cloud LoadBalancer implementation in your classpath. Add xref:spring-cloud-commons/loadbalancer.adoc#spring-cloud-loadbalancer-starter[Spring Cloud LoadBalancer starter] to your project in order to use it. +[[multiple-resttemplate-objects]] +=== Multiple `RestTemplate` Objects + +If you want a `RestTemplate` that is not load-balanced, create a `RestTemplate` bean and inject it. +To access the load-balanced `RestTemplate`, use the `@LoadBalanced` qualifier when you create your `@Bean`, as the following example shows: + +[source,java,indent=0] +---- +@Configuration +public class MyConfiguration { + + @LoadBalanced + @Bean + RestTemplate loadBalanced() { + return new RestTemplate(); + } + + @Primary + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } +} + +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); + } +} +---- + +IMPORTANT: Notice the use of the `@Primary` annotation on the plain `RestTemplate` declaration in the preceding example to disambiguate the unqualified `@Autowired` injection. + +TIP: If you see errors such as `java.lang.IllegalArgumentException: Can not set org.springframework.web.client.RestTemplate field com.my.app.Foo.restTemplate to com.sun.proxy.$Proxy89`, try injecting `RestOperations` or setting `spring.aop.proxyTargetClass=true`. + +[[rest-client-loadbalancer-client]] +== Spring `RestClient` as a LoadBalancer Client + +You can configure a `RestClient` to use a Load-balancer client. +To create a load-balanced `RestClient`, create a `RestClient.Builder` `@Bean` and use the `@LoadBalanced` qualifier, as the following example shows: + +[source,java,indent=0] +---- +@Configuration +public class MyConfiguration { + + @LoadBalanced + @Bean + RestClient.Builder restClientBuilder() { + return RestClient.builder(); + } +} + +public class MyClass { + + @Autowired + private RestClient.Builder restClientBuilder; + + public String doOtherStuff() { + return restClientBuilder.build().get().uri(URI.create("http://stores/stores")).retrieve().body(String.class); + } +} +---- + +The URI needs to use a virtual host name (that is, a service name, not a host name). +The `BlockingLoadBalancerClient` is used to create a full physical address. + +IMPORTANT: To use a load-balanced `RestClient`, you need to have a Spring LoadBalancer implementation in your classpath. +Add xref:spring-cloud-commons/loadbalancer.adoc#spring-cloud-loadbalancer-starter[Spring Cloud LoadBalancer starter] to your project in order to use it. + +[[multiple-restclient-objects]] +=== Multiple `RestClient.Builder` Objects + +If you want a `RestClient.Builder` that is not load-balanced, create a `RestClient.Builder` bean and inject it. +To access the load-balanced `RestClient`, use the `@LoadBalanced` qualifier when you create your `@Bean`, as the following example shows: + +[source,java,indent=0] +---- +@Configuration +public class MyConfiguration { + + @LoadBalanced + @Bean + RestClient.Builder loadBalanced() { + return RestClient.builder(); + } + + @Primary + @Bean + RestClient.Builder restClientBuilder() { + return WebClient.builder(); + } +} + +public class MyClass { + @Autowired + private RestClient.Builder restClientBuilder; + + @Autowired + @LoadBalanced + private RestClient.Builder loadBalanced; + + public String doOtherStuff() { + return restClientBuilder.build().get().uri("http://stores/stores") + .retrieve().body(String.class); + } + + public String doStuff() { + return restClientBuilder.build().get().uri("http://example.com") + .retrieve().body(String.class); + } +} +---- + +IMPORTANT: Notice the use of the `@Primary` annotation on the plain `RestTemplate` declaration in the preceding example to disambiguate the unqualified `@Autowired` injection. + [[webclinet-loadbalancer-client]] -== Spring WebClient as a Load Balancer Client +== Spring `WebClient` as a LoadBalancer Client You can configure `WebClient` to automatically use a load-balancer client. To create a load-balanced `WebClient`, create a `WebClient.Builder` `@Bean` and use the `@LoadBalanced` qualifier, as follows: @@ -204,11 +331,55 @@ public class MyClass { The URI needs to use a virtual host name (that is, a service name, not a host name). The Spring Cloud LoadBalancer is used to create a full physical address. -IMPORTANT: If you want to use a `@LoadBalanced WebClient.Builder`, you need to have a load balancer +IMPORTANT: If you want to use a `@LoadBalanced WebClient.Builder`, you need to have a Spring Cloud LoadBalancer implementation in the classpath. We recommend that you add the xref:spring-cloud-commons/loadbalancer.adoc#spring-cloud-loadbalancer-starter[Spring Cloud LoadBalancer starter] to your project. Then, `ReactiveLoadBalancer` is used underneath. +[[multiple-webclient-objects]] +=== Multiple `WebClient.Builder` Objects + +If you want a `WebClient.Buider` that is not load-balanced, create a `WebClient` bean and inject it. +To access the load-balanced `WebClient.Builder`, use the `@LoadBalanced` qualifier when you create your `@Bean`, as the following example shows: + +[source,java,indent=0] +---- +@Configuration +public class MyConfiguration { + + @LoadBalanced + @Bean + WebClient.Builder loadBalanced() { + return WebClient.builder(); + } + + @Primary + @Bean + WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } +} + +public class MyClass { + @Autowired + private WebClient.Builder webClientBuilder; + + @Autowired + @LoadBalanced + private WebClient.Builder loadBalanced; + + public Mono doOtherStuff() { + return loadBalanced.build().get().uri("http://stores/stores") + .retrieve().bodyToMono(String.class); + } + + public Mono doStuff() { + return webClientBuilder.build().get().uri("http://example.com") + .retrieve().bodyToMono(String.class); + } +} +---- + [[retrying-failed-requests]] === Retrying Failed Requests @@ -295,96 +466,6 @@ public class MyConfiguration { } ---- -[[multiple-resttemplate-objects]] -== Multiple `RestTemplate` Objects - -If you want a `RestTemplate` that is not load-balanced, create a `RestTemplate` bean and inject it. -To access the load-balanced `RestTemplate`, use the `@LoadBalanced` qualifier when you create your `@Bean`, as the following example shows: - -[source,java,indent=0] ----- -@Configuration -public class MyConfiguration { - - @LoadBalanced - @Bean - RestTemplate loadBalanced() { - return new RestTemplate(); - } - - @Primary - @Bean - RestTemplate restTemplate() { - return new RestTemplate(); - } -} - -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); - } -} ----- - -IMPORTANT: Notice the use of the `@Primary` annotation on the plain `RestTemplate` declaration in the preceding example to disambiguate the unqualified `@Autowired` injection. - -TIP: If you see errors such as `java.lang.IllegalArgumentException: Can not set org.springframework.web.client.RestTemplate field com.my.app.Foo.restTemplate to com.sun.proxy.$Proxy89`, try injecting `RestOperations` or setting `spring.aop.proxyTargetClass=true`. - -[[multiple-webclient-objects]] -== Multiple WebClient Objects - -If you want a `WebClient` that is not load-balanced, create a `WebClient` bean and inject it. -To access the load-balanced `WebClient`, use the `@LoadBalanced` qualifier when you create your `@Bean`, as the following example shows: - -[source,java,indent=0] ----- -@Configuration -public class MyConfiguration { - - @LoadBalanced - @Bean - WebClient.Builder loadBalanced() { - return WebClient.builder(); - } - - @Primary - @Bean - WebClient.Builder webClient() { - return WebClient.builder(); - } -} - -public class MyClass { - @Autowired - private WebClient.Builder webClientBuilder; - - @Autowired - @LoadBalanced - private WebClient.Builder loadBalanced; - - public Mono doOtherStuff() { - return loadBalanced.build().get().uri("http://stores/stores") - .retrieve().bodyToMono(String.class); - } - - public Mono doStuff() { - return webClientBuilder.build().get().uri("http://example.com") - .retrieve().bodyToMono(String.class); - } -} ----- - [[loadbalanced-webclient]] == Spring WebFlux `WebClient` as a Load Balancer Client diff --git a/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc b/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc index b8be4c33..1ecaffad 100644 --- a/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc @@ -44,12 +44,13 @@ NOTE: The classes you pass as `@LoadBalancerClient` or `@LoadBalancerClients` co [[spring-cloud-loadbalancer-integrations]] == Spring Cloud LoadBalancer integrations -In order to make it easy to use Spring Cloud LoadBalancer, we provide `ReactorLoadBalancerExchangeFilterFunction` that can be used with `WebClient` and `BlockingLoadBalancerClient` that works with `RestTemplate`. +In order to make it easy to use Spring Cloud LoadBalancer, we provide `ReactorLoadBalancerExchangeFilterFunction` that can be used with `WebClient` and `BlockingLoadBalancerClient` that works with `RestTemplate` and `RestClient`. You can see more information and examples of usage in the following sections: -* xref:spring-cloud-commons/common-abstractions.adoc#rest-template-loadbalancer-client[Spring RestTemplate as a Load Balancer Client] -* xref:spring-cloud-commons/common-abstractions.adoc#webclinet-loadbalancer-client[Spring WebClient as a Load Balancer Client] -* xref:spring-cloud-commons/common-abstractions.adoc#webflux-with-reactive-loadbalancer[Spring WebFlux WebClient with `ReactorLoadBalancerExchangeFilterFunction`] +* xref:spring-cloud-commons/common-abstractions.adoc#rest-template-loadbalancer-client[Spring `RestTemplate` as a LoadBalancer Client] +* xref:spring-cloud-commons/common-abstractions.adoc#rest-client-loadbalancer-client[Spring `RestClient` as a LoadBalancer Client] +* xref:spring-cloud-commons/common-abstractions.adoc#webclinet-loadbalancer-client[Spring `WebClient` as a LoadBalancer Client] +* xref:spring-cloud-commons/common-abstractions.adoc#webflux-with-reactive-loadbalancer[Spring `WebFlux WebClient` with `ReactorLoadBalancerExchangeFilterFunction`] [[loadbalancer-caching]] == Spring Cloud LoadBalancer Caching @@ -232,7 +233,7 @@ public class CustomLoadBalancerConfiguration { ---- TIP: For the non-reactive stack, create this supplier with the `withBlockingHealthChecks()`. -You can also pass your own `WebClient` or `RestTemplate` instance to be used for the checks. +You can also pass your own `WebClient`, `RestTemplate` or `RestClient` instance to be used for the checks. WARNING: `HealthCheckServiceInstanceListSupplier` has its own caching mechanism based on Reactor Flux `replay()`. Therefore, if it's being used, you may want to skip wrapping that supplier with `CachingServiceInstanceListSupplier`. @@ -336,7 +337,7 @@ public class CustomLoadBalancerConfiguration { You can use the selected `ServiceInstance` to transform the load-balanced HTTP Request. -For `RestTemplate`, you need to implement and define `LoadBalancerRequestTransformer` as follows: +For `RestTemplate` and `RestClient`, you need to implement and define `LoadBalancerRequestTransformer` as follows: [source,java,indent=0] ---- diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/BlockingRestClassesPresentCondition.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/BlockingRestClassesPresentCondition.java new file mode 100644 index 00000000..c6865ef0 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/BlockingRestClassesPresentCondition.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 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.cloud.client.loadbalancer; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; + +/** + * Verifies whether either {@link RestTemplate} or {@link RestClient} class is present. + * + * @author Olga Maciaszek-Sharma + * @since 4.1.0 + */ +public final class BlockingRestClassesPresentCondition extends AnyNestedCondition { + + private BlockingRestClassesPresentCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnClass(RestTemplate.class) + static class RestTemplatePresent { + + } + + @ConditionalOnClass(RestClient.class) + static class RestClientPresent { + + } + +} 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 index a09062ce..667e0192 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -26,8 +26,8 @@ import java.lang.annotation.Target; import org.springframework.beans.factory.annotation.Qualifier; /** - * Annotation to mark a RestTemplate or WebClient bean to be configured to use a - * LoadBalancerClient. + * Annotation to mark a RestTemplate, RestClient.Builder or WebClient.Builder bean to be + * configured to use a LoadBalancerClient. * @author Spencer Gibb */ @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) 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 7fdc57da..61522812 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -49,7 +50,7 @@ import org.springframework.web.client.RestTemplate; * @author Olga Maciaszek-Sharma */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass(RestTemplate.class) +@Conditional(BlockingRestClassesPresentCondition.class) @ConditionalOnBean(LoadBalancerClient.class) @EnableConfigurationProperties(LoadBalancerClientsProperties.class) public class LoadBalancerAutoConfiguration { @@ -99,6 +100,14 @@ public class LoadBalancerAutoConfiguration { }; } + @Bean + @ConditionalOnBean(LoadBalancerInterceptor.class) + @ConditionalOnMissingBean + LoadBalancerRestClientBuilderBeanPostProcessor lbRestClientPostProcessor( + final LoadBalancerInterceptor loadBalancerInterceptor, ApplicationContext context) { + return new LoadBalancerRestClientBuilderBeanPostProcessor(loadBalancerInterceptor, context); + } + } private static class RetryMissingOrDisabledCondition extends AnyNestedCondition { @@ -164,6 +173,14 @@ public class LoadBalancerAutoConfiguration { }; } + @Bean + @ConditionalOnBean(RetryLoadBalancerInterceptor.class) + @ConditionalOnMissingBean + LoadBalancerRestClientBuilderBeanPostProcessor lbRestClientPostProcessor( + final RetryLoadBalancerInterceptor loadBalancerInterceptor, ApplicationContext context) { + return new LoadBalancerRestClientBuilderBeanPostProcessor(loadBalancerInterceptor, context); + } + } } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRequestTransformer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRequestTransformer.java index 3efa567c..d66d3433 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRequestTransformer.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRequestTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -30,7 +30,7 @@ import org.springframework.http.HttpRequest; public interface LoadBalancerRequestTransformer { /** - * Order for the load balancer request tranformer. + * Order for the load balancer request transformer. */ int DEFAULT_ORDER = 0; diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientBuilderBeanPostProcessor.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientBuilderBeanPostProcessor.java new file mode 100644 index 00000000..6bd63cd7 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientBuilderBeanPostProcessor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2023 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.cloud.client.loadbalancer; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestClient; + +/** + * A {@link BeanPostProcessor} that adds the provided {@link ClientHttpRequestInterceptor} + * to all {@link RestClient.Builder} instances annotated with {@link LoadBalanced}. + * + * @author Olga Maciaszek-Sharma + * @since 4.1.0 + */ +public class LoadBalancerRestClientBuilderBeanPostProcessor implements BeanPostProcessor { + + private final ClientHttpRequestInterceptor loadBalancerInterceptor; + + private final ApplicationContext context; + + public LoadBalancerRestClientBuilderBeanPostProcessor(ClientHttpRequestInterceptor loadBalancerInterceptor, + ApplicationContext context) { + this.loadBalancerInterceptor = loadBalancerInterceptor; + this.context = context; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof RestClient.Builder) { + if (context.findAnnotationOnBean(beanName, LoadBalanced.class) == null) { + return bean; + } + ((RestClient.Builder) bean).requestInterceptor(loadBalancerInterceptor); + } + return bean; + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java index a4be354d..c81c40b4 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java @@ -24,18 +24,19 @@ import java.util.Random; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.BDDAssertions.then; import static org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer.REQUEST; @@ -46,49 +47,82 @@ import static org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoa */ public abstract class AbstractLoadBalancerAutoConfigurationTests { + protected ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(LoadBalancerAutoConfiguration.class)); + @Test - public void restTemplateGetsLoadBalancerInterceptor() { - ConfigurableApplicationContext context = init(OneRestTemplate.class); - final Map restTemplates = context.getBeansOfType(RestTemplate.class); + void restTemplateGetsLoadBalancerInterceptor() { + applicationContextRunner.withUserConfiguration(OneRestTemplate.class).run(context -> { + final Map restTemplates = context.getBeansOfType(RestTemplate.class); - then(restTemplates).isNotNull(); - then(restTemplates.values()).hasSize(1); - RestTemplate restTemplate = restTemplates.values().iterator().next(); - then(restTemplate).isNotNull(); + then(restTemplates).isNotNull(); + then(restTemplates.values()).hasSize(1); + RestTemplate restTemplate = restTemplates.values().iterator().next(); + then(restTemplate).isNotNull(); - assertLoadBalanced(restTemplate); + assertLoadBalanced(restTemplate); + }); } + @Test + void restClientBuilderWithLoadBalancerInterceptor() { + applicationContextRunner.withUserConfiguration(OneRestClientBuilder.class).run(context -> { + final Map restClientBuilders = context.getBeansOfType(RestClient.Builder.class); + + assertThat(restClientBuilders).isNotNull(); + assertThat(restClientBuilders).hasSize(1); + RestClient.Builder restClientBuilder = restClientBuilders.values().iterator().next(); + assertThat(restClientBuilder).isNotNull(); + assertLoadBalanced(restClientBuilder); + }); + } + + @Test + void multipleRestTemplates() { + applicationContextRunner.withUserConfiguration(TwoRestTemplatesAndTwoRestClientBuilders.class).run(context -> { + final Map restTemplates = context.getBeansOfType(RestTemplate.class); + + then(restTemplates).isNotNull(); + Collection templates = restTemplates.values(); + then(templates).hasSize(2); + + TwoRestTemplatesAndTwoRestClientBuilders.Two two = context + .getBean(TwoRestTemplatesAndTwoRestClientBuilders.Two.class); + + then(two.loadBalanced).isNotNull(); + assertLoadBalanced(two.loadBalanced); + + then(two.nonLoadBalanced).isNotNull(); + then(two.nonLoadBalanced.getInterceptors()).isEmpty(); + }); + } + + @Test + void multipleRestClientBuilders() { + applicationContextRunner.withUserConfiguration(TwoRestTemplatesAndTwoRestClientBuilders.class).run(context -> { + final Map restClientBuilders = context.getBeansOfType(RestClient.Builder.class); + + assertThat(restClientBuilders).isNotNull(); + assertThat(restClientBuilders.values()).hasSize(2); + + TwoRestTemplatesAndTwoRestClientBuilders.Two two = context + .getBean(TwoRestTemplatesAndTwoRestClientBuilders.Two.class); + + assertThat(two.loadBalancedRestClientBuilder).isNotNull(); + assertLoadBalanced(two.loadBalancedRestClientBuilder); + + assertThat(two.nonLoadBalancedRestClientBuilder).isNotNull(); + two.nonLoadBalancedRestClientBuilder + .requestInterceptors(interceptors -> assertThat(interceptors).isEmpty()); + }); + } + + protected abstract void assertLoadBalanced(RestClient.Builder restClientBuilder); + protected abstract void assertLoadBalanced(RestTemplate restTemplate); - @Test - public void multipleRestTemplates() { - ConfigurableApplicationContext context = init(TwoRestTemplates.class); - final Map restTemplates = context.getBeansOfType(RestTemplate.class); - - then(restTemplates).isNotNull(); - Collection templates = restTemplates.values(); - then(templates).hasSize(2); - - TwoRestTemplates.Two two = context.getBean(TwoRestTemplates.Two.class); - - then(two.loadBalanced).isNotNull(); - assertLoadBalanced(two.loadBalanced); - - then(two.nonLoadBalanced).isNotNull(); - then(two.nonLoadBalanced.getInterceptors()).isEmpty(); - } - - protected ConfigurableApplicationContext init(Class config) { - return init(config, "spring.aop.proxyTargetClass=true"); - } - - protected ConfigurableApplicationContext init(Class config, String... props) { - return new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(props) - .sources(config, LoadBalancerAutoConfiguration.class).run(); - } - @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) protected static class OneRestTemplate { @LoadBalanced @@ -97,6 +131,23 @@ public abstract class AbstractLoadBalancerAutoConfigurationTests { return new RestTemplate(); } + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + protected static class OneRestClientBuilder { + + @LoadBalanced + @Bean + RestClient.Builder loadBalancedRestClientBuilder() { + return RestClient.builder(); + } + + } + + @Configuration(proxyBeanMethods = false) + protected static class BaseConfiguration { + @Bean LoadBalancerClient loadBalancerClient() { return new NoopLoadBalancerClient(); @@ -110,8 +161,8 @@ public abstract class AbstractLoadBalancerAutoConfigurationTests { } @Configuration(proxyBeanMethods = false) - @Import(OneRestTemplate.class) - protected static class TwoRestTemplates { + @Import({ OneRestTemplate.class, OneRestClientBuilder.class }) + protected static class TwoRestTemplatesAndTwoRestClientBuilders { @Primary @Bean @@ -119,6 +170,12 @@ public abstract class AbstractLoadBalancerAutoConfigurationTests { return new RestTemplate(); } + @Primary + @Bean + RestClient.Builder restClientBuilder() { + return RestClient.builder(); + } + @Configuration(proxyBeanMethods = false) protected static class Two { @@ -129,6 +186,13 @@ public abstract class AbstractLoadBalancerAutoConfigurationTests { @LoadBalanced RestTemplate loadBalanced; + @Autowired + RestClient.Builder nonLoadBalancedRestClientBuilder; + + @Autowired + @LoadBalanced + RestClient.Builder loadBalancedRestClientBuilder; + } } 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 index 40436e92..2585289a 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -20,12 +20,15 @@ import java.util.List; import org.springframework.cloud.test.ClassPathExclusions; import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.BDDAssertions.then; /** * @author Spencer Gibb + * @author Olga Maciaszek-Sharma */ @ClassPathExclusions({ "spring-retry-*.jar", "spring-boot-starter-aop-*.jar" }) public class LoadBalancerAutoConfigurationTests extends AbstractLoadBalancerAutoConfigurationTests { @@ -38,4 +41,12 @@ public class LoadBalancerAutoConfigurationTests extends AbstractLoadBalancerAuto then(interceptor).isInstanceOf(LoadBalancerInterceptor.class); } + @Override + protected void assertLoadBalanced(RestClient.Builder restClientBuilder) { + restClientBuilder.requestInterceptors(interceptors -> { + assertThat(interceptors).hasSize(1); + assertThat(interceptors.get(0)).isInstanceOf(LoadBalancerInterceptor.class); + }); + } + } diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerAutoConfigurationTests.java index 2f1336e6..6fe21781 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerAutoConfigurationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerAutoConfigurationTests.java @@ -20,15 +20,17 @@ import java.util.List; import org.junit.jupiter.api.Test; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.retry.backoff.NoBackOffPolicy; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.BDDAssertions.then; /** * @author Ryan Baxter + * @author Olga Maciaszek-Sharma */ public class RetryLoadBalancerAutoConfigurationTests extends AbstractLoadBalancerAutoConfigurationTests { @@ -40,22 +42,48 @@ public class RetryLoadBalancerAutoConfigurationTests extends AbstractLoadBalance then(interceptor).isInstanceOf(RetryLoadBalancerInterceptor.class); } - @Test - public void testRetryDisabled() { - ConfigurableApplicationContext context = init(OneRestTemplate.class, "spring.aop.proxyTargetClass=true", - "spring.cloud.loadbalancer.retry.enabled=false"); - List interceptors = context.getBean(RestTemplate.class).getInterceptors(); - then(interceptors).hasSize(1); - ClientHttpRequestInterceptor interceptor = interceptors.get(0); - then(interceptor).isInstanceOf(LoadBalancerInterceptor.class); + @Override + protected void assertLoadBalanced(RestClient.Builder restClientBuilder) { + restClientBuilder.requestInterceptors(interceptors -> { + assertThat(interceptors).hasSize(1); + assertThat(interceptors.get(0)).isInstanceOf(RetryLoadBalancerInterceptor.class); + }); } @Test - public void testDefaultBackOffPolicy() { - ConfigurableApplicationContext context = init(OneRestTemplate.class); - LoadBalancedRetryFactory loadBalancedRetryFactory = context.getBean(LoadBalancedRetryFactory.class); - then(loadBalancedRetryFactory).isInstanceOf(LoadBalancedRetryFactory.class); - then(loadBalancedRetryFactory.createBackOffPolicy("foo")).isInstanceOf(NoBackOffPolicy.class); + void testRetryDisabled() { + applicationContextRunner.withUserConfiguration(OneRestTemplate.class) + .withPropertyValues("spring.aop.proxyTargetClass=true", "spring.cloud.loadbalancer.retry.enabled=false") + .run(context -> { + List interceptors = context.getBean(RestTemplate.class) + .getInterceptors(); + then(interceptors).hasSize(1); + ClientHttpRequestInterceptor interceptor = interceptors.get(0); + then(interceptor).isInstanceOf(LoadBalancerInterceptor.class); + }); + } + + @Test + void testRetryDisabledWithRestClientBuilder() { + applicationContextRunner.withUserConfiguration(OneRestClientBuilder.class) + .withPropertyValues("spring.aop.proxyTargetClass=true", "spring.cloud.loadbalancer.retry.enabled=false") + .run(context -> { + RestClient.Builder restClientBuilder = context.getBean(RestClient.Builder.class); + + restClientBuilder.requestInterceptors(interceptors -> { + assertThat(interceptors).hasSize(1); + assertThat(interceptors.get(0)).isInstanceOf(LoadBalancerInterceptor.class); + }); + }); + } + + @Test + void testDefaultBackOffPolicy() { + applicationContextRunner.withUserConfiguration(OneRestTemplate.class).run(context -> { + LoadBalancedRetryFactory loadBalancedRetryFactory = context.getBean(LoadBalancedRetryFactory.class); + then(loadBalancedRetryFactory).isInstanceOf(LoadBalancedRetryFactory.class); + then(loadBalancedRetryFactory.createBackOffPolicy("foo")).isInstanceOf(NoBackOffPolicy.class); + }); } } diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java index 8e7a5683..abf47dab 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java @@ -48,6 +48,7 @@ import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.retry.support.RetryTemplate; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -188,6 +189,16 @@ public class LoadBalancerClientConfiguration { .build(context); } + @Bean + @ConditionalOnBean({ DiscoveryClient.class, RestClient.class }) + @ConditionalOnMissingBean + @Conditional(HealthCheckConfigurationCondition.class) + public ServiceInstanceListSupplier healthCheckRestClientDiscoveryClientServiceInstanceListSupplier( + ConfigurableApplicationContext context) { + return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient() + .withBlockingRestClientHealthChecks().build(context); + } + @Bean @ConditionalOnBean(DiscoveryClient.class) @ConditionalOnMissingBean diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java index bf7fe4ad..158c41bd 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.BlockingRestClassesPresentCondition; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; @@ -35,9 +36,9 @@ import org.springframework.cloud.loadbalancer.blocking.retry.BlockingLoadBalance import org.springframework.cloud.loadbalancer.core.LoadBalancerServiceInstanceCookieTransformer; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.retry.support.RetryTemplate; -import org.springframework.web.client.RestTemplate; /** * An autoconfiguration for {@link BlockingLoadBalancerClient}. @@ -50,7 +51,7 @@ import org.springframework.web.client.RestTemplate; @LoadBalancerClients @AutoConfigureAfter(LoadBalancerAutoConfiguration.class) @AutoConfigureBefore({ org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.class }) -@ConditionalOnClass(RestTemplate.class) +@Conditional(BlockingRestClassesPresentCondition.class) @ConditionalOnProperty(value = "spring.cloud.loadbalancer.enabled", havingValue = "true", matchIfMissing = true) public class BlockingLoadBalancerClientAutoConfiguration { diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java index ff40abf8..d7e97c4e 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java @@ -39,6 +39,7 @@ import org.springframework.core.env.PropertyResolver; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; @@ -203,6 +204,21 @@ public final class ServiceInstanceListSupplierBuilder { return this; } + /** + * Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided + * {@link RestClient} instance to the {@link ServiceInstanceListSupplier} hierarchy. + * @return the {@link ServiceInstanceListSupplierBuilder} object + */ + public ServiceInstanceListSupplierBuilder withBlockingRestClientHealthChecks() { + DelegateCreator creator = (context, delegate) -> { + RestClient restClient = context.getBean(RestClient.class); + LoadBalancerClientFactory loadBalancerClientFactory = context.getBean(LoadBalancerClientFactory.class); + return blockingHealthCheckServiceInstanceListSupplier(restClient, delegate, loadBalancerClientFactory); + }; + this.creators.add(creator); + return this; + } + /** * Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided * {@link RestTemplate} instance to the {@link ServiceInstanceListSupplier} hierarchy. @@ -218,6 +234,21 @@ public final class ServiceInstanceListSupplierBuilder { return this; } + /** + * Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided + * {@link RestClient} instance to the {@link ServiceInstanceListSupplier} hierarchy. + * @param restClient a user-provided {@link RestClient} instance + * @return the {@link ServiceInstanceListSupplierBuilder} object + */ + public ServiceInstanceListSupplierBuilder withBlockingHealthChecks(RestClient restClient) { + DelegateCreator creator = (context, delegate) -> { + LoadBalancerClientFactory loadBalancerClientFactory = context.getBean(LoadBalancerClientFactory.class); + return blockingHealthCheckServiceInstanceListSupplier(restClient, delegate, loadBalancerClientFactory); + }; + this.creators.add(creator); + return this; + } + /** * Adds a {@link ZonePreferenceServiceInstanceListSupplier} to the * {@link ServiceInstanceListSupplier} hierarchy. @@ -371,6 +402,22 @@ public final class ServiceInstanceListSupplierBuilder { })); } + private ServiceInstanceListSupplier blockingHealthCheckServiceInstanceListSupplier(RestClient restClient, + ServiceInstanceListSupplier delegate, LoadBalancerClientFactory loadBalancerClientFactory) { + return new HealthCheckServiceInstanceListSupplier(delegate, loadBalancerClientFactory, + (serviceInstance, healthCheckPath) -> Mono.defer(() -> { + URI uri = UriComponentsBuilder.fromUriString(getUri(serviceInstance, healthCheckPath)).build() + .toUri(); + try { + return Mono.just(HttpStatus.OK + .equals(restClient.get().uri(uri).retrieve().toBodilessEntity().getStatusCode())); + } + catch (Exception ignored) { + return Mono.just(false); + } + })); + } + static String getUri(ServiceInstance serviceInstance, String healthCheckPath) { if (StringUtils.hasText(healthCheckPath)) { String path = healthCheckPath.startsWith("/") ? healthCheckPath : "/" + healthCheckPath; diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationTests.java index eda9dc1c..b455d858 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationTests.java @@ -16,7 +16,12 @@ package org.springframework.cloud.loadbalancer.annotation; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -38,6 +43,7 @@ import org.springframework.cloud.loadbalancer.core.ZonePreferenceServiceInstance import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.retry.support.RetryTemplate; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -191,9 +197,10 @@ class LoadBalancerClientConfigurationTests { } - @Test - void shouldInstantiateBlockingHealthCheckServiceInstanceListSupplier() { - blockingDiscoveryClientRunner.withUserConfiguration(RestTemplateTestConfig.class) + @ParameterizedTest + @MethodSource("blockingConfigurations") + void shouldInstantiateBlockingHealthCheckServiceInstanceListSupplier(Class configurationClass) { + blockingDiscoveryClientRunner.withUserConfiguration(configurationClass) .withPropertyValues("spring.cloud.loadbalancer.configurations=health-check").run(context -> { ServiceInstanceListSupplier supplier = context.getBean(ServiceInstanceListSupplier.class); then(supplier).isInstanceOf(HealthCheckServiceInstanceListSupplier.class); @@ -204,8 +211,8 @@ class LoadBalancerClientConfigurationTests { @Test void shouldInstantiateBlockingWeightedServiceInstanceListSupplier() { - blockingDiscoveryClientRunner.withUserConfiguration(RestTemplateTestConfig.class) - .withPropertyValues("spring.cloud.loadbalancer.configurations=weighted").run(context -> { + blockingDiscoveryClientRunner.withPropertyValues("spring.cloud.loadbalancer.configurations=weighted") + .run(context -> { ServiceInstanceListSupplier supplier = context.getBean(ServiceInstanceListSupplier.class); then(supplier).isInstanceOf(WeightedServiceInstanceListSupplier.class); ServiceInstanceListSupplier delegate = ((DelegatingServiceInstanceListSupplier) supplier) @@ -216,6 +223,11 @@ class LoadBalancerClientConfigurationTests { }); } + private static Stream blockingConfigurations() { + return Stream.of(Arguments.of(RestTemplateTestConfig.class), Arguments.of(RestClientTestConfig.class), + Arguments.of(RestTemplateAndRestClientConfig.class)); + } + @Configuration protected static class TestConfig { @@ -237,4 +249,29 @@ class LoadBalancerClientConfigurationTests { } + @Configuration + protected static class RestClientTestConfig { + + @Bean + RestClient restClient() { + return RestClient.create(); + } + + } + + @Configuration + protected static class RestTemplateAndRestClientConfig { + + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + RestClient restClient() { + return RestClient.create(); + } + + } + } diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfigurationTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfigurationTests.java index 9558d56d..9f02fd7d 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfigurationTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2023 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. @@ -23,6 +23,7 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; @@ -48,7 +49,7 @@ class BlockingLoadBalancerClientAutoConfigurationTests { @Test public void worksWithoutSpringWeb() { - applicationContextRunner.withClassLoader(new FilteredClassLoader(RestTemplate.class)) + applicationContextRunner.withClassLoader(new FilteredClassLoader(RestTemplate.class, RestClient.class)) .run(context -> assertThat(context).doesNotHaveBean(BlockingLoadBalancerClient.class)); } diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java index d2bb0eea..9576d436 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -22,6 +22,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.assertj.core.util.Lists; @@ -29,6 +30,9 @@ import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -47,6 +51,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -79,10 +84,6 @@ class HealthCheckServiceInstanceListSupplierTests { @LocalServerPort private int port; - private final WebClient webClient = WebClient.create(); - - private final RestTemplate restTemplate = new RestTemplate(); - private LoadBalancerProperties properties; private HealthCheckServiceInstanceListSupplier listSupplier; @@ -109,7 +110,7 @@ class HealthCheckServiceInstanceListSupplierTests { false); listSupplier = new HealthCheckServiceInstanceListSupplier( ServiceInstanceListSuppliers.from(serviceId, serviceInstance), - buildLoadBalancerClientFactory(serviceId, properties), healthCheckFunction(webClient)); + buildLoadBalancerClientFactory(serviceId, properties), webClientHealthCheckFunction()); boolean alive = listSupplier.isAlive(serviceInstance).block(); @@ -151,15 +152,17 @@ class HealthCheckServiceInstanceListSupplierTests { } @SuppressWarnings("ConstantConditions") - @Test - void shouldCheckInstanceWithProvidedHealthCheckPathWithRestTemplate() { + @ParameterizedTest + @MethodSource("healthCheckFunctions") + void shouldCheckInstanceWithProvidedHealthCheckPath( + BiFunction> healthCheckFunction) { String serviceId = "ignored-service"; properties.getHealthCheck().getPath().put("ignored-service", "/health"); ServiceInstance serviceInstance = new DefaultServiceInstance("ignored-service-1", serviceId, "127.0.0.1", port, false); listSupplier = new HealthCheckServiceInstanceListSupplier( ServiceInstanceListSuppliers.from(serviceId, serviceInstance), - buildLoadBalancerClientFactory(serviceId, properties), healthCheckFunction(restTemplate)); + buildLoadBalancerClientFactory(serviceId, properties), healthCheckFunction); boolean alive = listSupplier.isAlive(serviceInstance).block(); @@ -167,46 +170,16 @@ class HealthCheckServiceInstanceListSupplierTests { } @SuppressWarnings("ConstantConditions") - @Test - void shouldCheckInstanceWithDefaultHealthCheckPath() { - String serviceId = "ignored-service"; - ServiceInstance serviceInstance = new DefaultServiceInstance("ignored-service-1", serviceId, "127.0.0.1", port, - false); - listSupplier = new HealthCheckServiceInstanceListSupplier( - ServiceInstanceListSuppliers.from(serviceId, serviceInstance), - buildLoadBalancerClientFactory(serviceId, properties), healthCheckFunction(webClient)); - - boolean alive = listSupplier.isAlive(serviceInstance).block(); - - assertThat(alive).isTrue(); - } - - @SuppressWarnings("ConstantConditions") - @Test - void shouldReturnFalseIfEndpointNotFound() { + @ParameterizedTest + @MethodSource("healthCheckFunctions") + void shouldReturnFalseIfEndpointNotFound(BiFunction> healthCheckFunction) { String serviceId = "ignored-service"; ServiceInstance serviceInstance = new DefaultServiceInstance("ignored-service-1", serviceId, "127.0.0.1", port, false); properties.getHealthCheck().getPath().put(serviceId, "/test"); listSupplier = new HealthCheckServiceInstanceListSupplier( ServiceInstanceListSuppliers.from(serviceId, serviceInstance), - buildLoadBalancerClientFactory(serviceId, properties), healthCheckFunction(webClient)); - - boolean alive = listSupplier.isAlive(serviceInstance).block(); - - assertThat(alive).isFalse(); - } - - @SuppressWarnings("ConstantConditions") - @Test - void shouldReturnFalseIfEndpointNotFoundWithRestTemplate() { - String serviceId = "ignored-service"; - ServiceInstance serviceInstance = new DefaultServiceInstance("ignored-service-1", serviceId, "127.0.0.1", port, - false); - properties.getHealthCheck().getPath().put(serviceId, "/test"); - listSupplier = new HealthCheckServiceInstanceListSupplier( - ServiceInstanceListSuppliers.from(serviceId, serviceInstance), - buildLoadBalancerClientFactory(serviceId, properties), healthCheckFunction(restTemplate)); + buildLoadBalancerClientFactory(serviceId, properties), healthCheckFunction); boolean alive = listSupplier.isAlive(serviceInstance).block(); @@ -232,7 +205,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.doReturn(Mono.just(false)).when(mock).isAlive(serviceInstance2); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return mock.isAlive(serviceInstance); @@ -263,7 +236,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.doReturn(Mono.just(true)).when(mock).isAlive(serviceInstance2); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return mock.isAlive(serviceInstance); @@ -296,7 +269,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.doReturn(Mono.just(true)).when(mock).isAlive(serviceInstance2); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return mock.isAlive(serviceInstance); @@ -327,7 +300,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.doReturn(Mono.error(new RuntimeException("boom"))).when(mock).isAlive(serviceInstance2); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return mock.isAlive(serviceInstance); @@ -353,7 +326,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID); Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(serviceInstance1, serviceInstance2))); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { if (serviceInstance == serviceInstance1) { @@ -381,7 +354,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.when(delegate.getServiceId()).thenReturn(SERVICE_ID); Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(serviceInstance1))); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return Mono.just(true); @@ -414,7 +387,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.doReturn(Mono.error(new RuntimeException("boom"))).when(mock).isAlive(serviceInstance2); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return mock.isAlive(serviceInstance); @@ -443,7 +416,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.when(mock.isAlive(serviceInstance1)).thenReturn(Mono.never(), Mono.just(true)); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return mock.isAlive(serviceInstance); @@ -475,7 +448,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.when(delegate.get()).thenReturn(instances); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return Mono.just(true); @@ -510,7 +483,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.when(delegate.get()).thenReturn(instances); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return Mono.just(true); @@ -543,7 +516,7 @@ class HealthCheckServiceInstanceListSupplierTests { when(delegate.get()).thenReturn(Flux.just(Collections.singletonList(serviceInstance1))) .thenReturn(Flux.just(Collections.singletonList(serviceInstance2))); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return Mono.just(true); @@ -572,7 +545,7 @@ class HealthCheckServiceInstanceListSupplierTests { when(delegate.getServiceId()).thenReturn(SERVICE_ID); when(delegate.get()).thenReturn(Flux.just(Collections.singletonList(serviceInstance1))) .thenReturn(Flux.just(Collections.singletonList(serviceInstance2))); - BiFunction> healthCheckFunc = healthCheckFunction(webClient); + BiFunction> healthCheckFunc = webClientHealthCheckFunction(); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunc) { @Override @@ -601,7 +574,7 @@ class HealthCheckServiceInstanceListSupplierTests { Mockito.when(delegate.get()).thenReturn(Flux.just(Lists.list(serviceInstance1))); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)) { + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()) { @Override protected Mono isAlive(ServiceInstance serviceInstance) { return Mono.just(true); @@ -633,7 +606,7 @@ class HealthCheckServiceInstanceListSupplierTests { .doOnSubscribe(subscription -> subscribed.set(true)).doOnCancel(instancesCanceled::incrementAndGet)); listSupplier = new HealthCheckServiceInstanceListSupplier(delegate, - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)); + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()); listSupplier.afterPropertiesSet(); @@ -656,7 +629,7 @@ class HealthCheckServiceInstanceListSupplierTests { false); listSupplier = new HealthCheckServiceInstanceListSupplier( ServiceInstanceListSuppliers.from(serviceId, serviceInstance), - buildLoadBalancerClientFactory(SERVICE_ID, properties), healthCheckFunction(webClient)); + buildLoadBalancerClientFactory(SERVICE_ID, properties), webClientHealthCheckFunction()); boolean alive = listSupplier.isAlive(serviceInstance).block(); @@ -674,7 +647,7 @@ class HealthCheckServiceInstanceListSupplierTests { port, false); listSupplier = new HealthCheckServiceInstanceListSupplier( ServiceInstanceListSuppliers.from(serviceId, serviceInstance), loadBalancerClientFactory, - healthCheckFunction(webClient)); + webClientHealthCheckFunction()); listSupplier.isAlive(serviceInstance).block(); }); @@ -682,6 +655,18 @@ class HealthCheckServiceInstanceListSupplierTests { assertThat(exception).hasMessageContaining("Connection refused: /127.0.0.1:888"); } + private static Stream healthCheckFunctions() { + RestTemplate restTemplate = new RestTemplate(); + RestClient restClient = RestClient.create(); + return Stream.of(Arguments.of(healthCheckFunction(restTemplate)), Arguments.of(healthCheckFunction(restClient)), + Arguments.of(webClientHealthCheckFunction())); + } + + private static BiFunction> webClientHealthCheckFunction() { + WebClient webClient = WebClient.create(); + return healthCheckFunction(webClient); + } + @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration @RestController diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSuppliersTestUtils.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSuppliersTestUtils.java index 63ef3f77..0635c756 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSuppliersTestUtils.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSuppliersTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2023 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. @@ -23,6 +23,7 @@ import reactor.core.publisher.Mono; import org.springframework.cloud.client.ServiceInstance; import org.springframework.http.HttpStatus; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; @@ -61,4 +62,17 @@ final class ServiceInstanceListSuppliersTestUtils { }); } + static BiFunction> healthCheckFunction(RestClient restClient) { + return (serviceInstance, healthCheckPath) -> Mono.defer(() -> { + URI uri = UriComponentsBuilder.fromUriString(getUri(serviceInstance, healthCheckPath)).build().toUri(); + try { + return Mono.just( + HttpStatus.OK.equals(restClient.get().uri(uri).retrieve().toBodilessEntity().getStatusCode())); + } + catch (Exception ignored) { + return Mono.just(false); + } + }); + } + }