@LoadBalanced RestClient (#1294)

This commit is contained in:
Olga Maciaszek-Sharma
2023-11-13 18:12:19 +01:00
committed by GitHub
parent 42ba0eef31
commit dd47ca6c29
17 changed files with 634 additions and 235 deletions

View File

@@ -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<String> doOtherStuff() {
return loadBalanced.build().get().uri("http://stores/stores")
.retrieve().bodyToMono(String.class);
}
public Mono<String> 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<String> doOtherStuff() {
return loadBalanced.build().get().uri("http://stores/stores")
.retrieve().bodyToMono(String.class);
}
public Mono<String> doStuff() {
return webClientBuilder.build().get().uri("http://example.com")
.retrieve().bodyToMono(String.class);
}
}
----
[[loadbalanced-webclient]]
== Spring WebFlux `WebClient` as a Load Balancer Client

View File

@@ -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]
----

View File

@@ -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 {
}
}

View File

@@ -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 })

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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<String, RestTemplate> restTemplates = context.getBeansOfType(RestTemplate.class);
void restTemplateGetsLoadBalancerInterceptor() {
applicationContextRunner.withUserConfiguration(OneRestTemplate.class).run(context -> {
final Map<String, RestTemplate> 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<String, RestClient.Builder> 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<String, RestTemplate> restTemplates = context.getBeansOfType(RestTemplate.class);
then(restTemplates).isNotNull();
Collection<RestTemplate> 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<String, RestClient.Builder> 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<String, RestTemplate> restTemplates = context.getBeansOfType(RestTemplate.class);
then(restTemplates).isNotNull();
Collection<RestTemplate> 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;
}
}

View File

@@ -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);
});
}
}

View File

@@ -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<ClientHttpRequestInterceptor> 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<ClientHttpRequestInterceptor> 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);
});
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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<Arguments> 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();
}
}
}

View File

@@ -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));
}

View File

@@ -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<ServiceInstance, String, Mono<Boolean>> 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<ServiceInstance, String, Mono<Boolean>> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<ServiceInstance, String, Mono<Boolean>> healthCheckFunc = healthCheckFunction(webClient);
BiFunction<ServiceInstance, String, Mono<Boolean>> 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<Boolean> 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<Arguments> 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<ServiceInstance, String, Mono<Boolean>> webClientHealthCheckFunction() {
WebClient webClient = WebClient.create();
return healthCheckFunction(webClient);
}
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@RestController

View File

@@ -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<ServiceInstance, String, Mono<Boolean>> 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);
}
});
}
}