diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/DefaultFeignLoadBalancerConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/DefaultFeignLoadBalancerConfiguration.java index 98fa40c8..19fc0eef 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/DefaultFeignLoadBalancerConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/DefaultFeignLoadBalancerConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +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; @@ -44,8 +45,10 @@ class DefaultFeignLoadBalancerConfiguration { @Bean @ConditionalOnMissingBean @Conditional(OnRetryNotEnabledCondition.class) - public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties) { - return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties); + public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties, + LoadBalancerClientFactory loadBalancerClientFactory) { + return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties, + loadBalancerClientFactory); } @Bean @@ -55,9 +58,10 @@ class DefaultFeignLoadBalancerConfiguration { @ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true", matchIfMissing = true) public Client feignRetryClient(LoadBalancerClient loadBalancerClient, - LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) { + LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties, + LoadBalancerClientFactory loadBalancerClientFactory) { return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, - loadBalancedRetryFactory, properties); + loadBalancedRetryFactory, properties, loadBalancerClientFactory); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java index 0e9c5bbb..8427a3ae 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java @@ -19,6 +19,7 @@ package org.springframework.cloud.openfeign.loadbalancer; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Set; import feign.Client; import feign.Request; @@ -27,11 +28,18 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.CompletionContext; import org.springframework.cloud.client.loadbalancer.DefaultRequest; import org.springframework.cloud.client.loadbalancer.DefaultRequestContext; +import org.springframework.cloud.client.loadbalancer.DefaultResponse; +import org.springframework.cloud.client.loadbalancer.HttpRequestContext; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle; +import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator; import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.Assert; /** @@ -41,6 +49,7 @@ import org.springframework.util.Assert; * @author Olga Maciaszek-Sharma * @since 2.2.0 */ +@SuppressWarnings({ "unchecked", "rawtypes" }) public class FeignBlockingLoadBalancerClient implements Client { private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class); @@ -51,11 +60,14 @@ public class FeignBlockingLoadBalancerClient implements Client { private final LoadBalancerProperties properties; + private final LoadBalancerClientFactory loadBalancerClientFactory; + public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, - LoadBalancerProperties properties) { + LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) { this.delegate = delegate; this.loadBalancerClient = loadBalancerClient; this.properties = properties; + this.loadBalancerClientFactory = loadBalancerClientFactory; } @Override @@ -66,19 +78,47 @@ public class FeignBlockingLoadBalancerClient implements Client { String hint = getHint(serviceId); DefaultRequest lbRequest = new DefaultRequest<>( new DefaultRequestContext(request, hint)); + Set supportedLifecycleProcessors = LoadBalancerLifecycleValidator + .getSupportedLifecycleProcessors( + loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class), + HttpRequestContext.class, ClientHttpResponse.class, ServiceInstance.class); + supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest)); ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest); + org.springframework.cloud.client.loadbalancer.Response lbResponse = new DefaultResponse( + instance); if (instance == null) { String message = "Load balancer does not contain an instance for the service " + serviceId; if (LOG.isWarnEnabled()) { LOG.warn(message); } + supportedLifecycleProcessors.forEach( + lifecycle -> lifecycle.onComplete(new CompletionContext( + CompletionContext.Status.DISCARD, lbResponse))); return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value()) .body(message, StandardCharsets.UTF_8).build(); } String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString(); Request newRequest = Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(), request.charset(), request.requestTemplate()); - return delegate.execute(newRequest, options); + return executeWithLoadBalancerLifecycleProcessing(options, supportedLifecycleProcessors, lbResponse, + newRequest); + } + + private Response executeWithLoadBalancerLifecycleProcessing(Request.Options options, + Set supportedLifecycleProcessors, + org.springframework.cloud.client.loadbalancer.Response lbResponse, Request newRequest) + throws IOException { + try { + Response response = delegate.execute(newRequest, options); + supportedLifecycleProcessors.forEach(lifecycle -> lifecycle + .onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS, lbResponse, response))); + return response; + } + catch (Exception exception) { + supportedLifecycleProcessors.forEach(lifecycle -> lifecycle + .onComplete(new CompletionContext<>(CompletionContext.Status.FAILED, exception, lbResponse))); + throw exception; + } } // Visible for Sleuth instrumentation diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java index 0c9ef8fa..3075cca1 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.cloud.openfeign.FeignAutoConfiguration; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; import org.springframework.context.annotation.Configuration; @@ -37,7 +38,7 @@ import org.springframework.context.annotation.Import; * @since 2.2.0 */ @ConditionalOnClass(Feign.class) -@ConditionalOnBean(LoadBalancerClient.class) +@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class }) @AutoConfigureBefore(FeignAutoConfiguration.class) @EnableConfigurationProperties(FeignHttpClientProperties.class) @Configuration(proxyBeanMethods = false) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClientFeignLoadBalancerConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClientFeignLoadBalancerConfiguration.java index 4ab677e2..75c98fa6 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClientFeignLoadBalancerConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClientFeignLoadBalancerConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -43,7 +44,7 @@ import org.springframework.context.annotation.Import; */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ApacheHttpClient.class) -@ConditionalOnBean(LoadBalancerClient.class) +@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class }) @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true) @Import(HttpClientFeignConfiguration.class) @EnableConfigurationProperties(LoadBalancerProperties.class) @@ -53,9 +54,9 @@ class HttpClientFeignLoadBalancerConfiguration { @ConditionalOnMissingBean @Conditional(OnRetryNotEnabledCondition.class) public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient, - LoadBalancerProperties properties) { + LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) { ApacheHttpClient delegate = new ApacheHttpClient(httpClient); - return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties); + return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory); } @Bean @@ -65,10 +66,11 @@ class HttpClientFeignLoadBalancerConfiguration { @ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true", matchIfMissing = true) public Client feignRetryClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient, - LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) { + LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties, + LoadBalancerClientFactory loadBalancerClientFactory) { ApacheHttpClient delegate = new ApacheHttpClient(httpClient); return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory, - properties); + properties, loadBalancerClientFactory); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/OkHttpFeignLoadBalancerConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/OkHttpFeignLoadBalancerConfiguration.java index 224901d9..f8a58721 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/OkHttpFeignLoadBalancerConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/OkHttpFeignLoadBalancerConfiguration.java @@ -27,6 +27,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.cloud.openfeign.clientconfig.OkHttpFeignConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -43,7 +44,7 @@ import org.springframework.context.annotation.Import; @Configuration(proxyBeanMethods = false) @ConditionalOnClass(OkHttpClient.class) @ConditionalOnProperty("feign.okhttp.enabled") -@ConditionalOnBean(LoadBalancerClient.class) +@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class }) @Import(OkHttpFeignConfiguration.class) @EnableConfigurationProperties(LoadBalancerProperties.class) class OkHttpFeignLoadBalancerConfiguration { @@ -52,9 +53,9 @@ class OkHttpFeignLoadBalancerConfiguration { @ConditionalOnMissingBean @Conditional(OnRetryNotEnabledCondition.class) public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient loadBalancerClient, - LoadBalancerProperties properties) { + LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) { OkHttpClient delegate = new OkHttpClient(okHttpClient); - return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties); + return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory); } @Bean @@ -64,10 +65,11 @@ class OkHttpFeignLoadBalancerConfiguration { @ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true", matchIfMissing = true) public Client feignRetryClient(LoadBalancerClient loadBalancerClient, okhttp3.OkHttpClient okHttpClient, - LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) { + LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties, + LoadBalancerClientFactory loadBalancerClientFactory) { OkHttpClient delegate = new OkHttpClient(okHttpClient); return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory, - properties); + properties, loadBalancerClientFactory); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClient.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClient.java index 530ca65f..e1a6b6b1 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClient.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClient.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import feign.Client; import feign.Request; @@ -31,19 +32,26 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.CompletionContext; import org.springframework.cloud.client.loadbalancer.DefaultRequest; +import org.springframework.cloud.client.loadbalancer.DefaultResponse; +import org.springframework.cloud.client.loadbalancer.HttpRequestContext; import org.springframework.cloud.client.loadbalancer.InterceptorRetryPolicy; import org.springframework.cloud.client.loadbalancer.LoadBalancedRecoveryCallback; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle; +import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator; import org.springframework.cloud.client.loadbalancer.RetryableRequestContext; import org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException; import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpResponse; import org.springframework.retry.RetryListener; import org.springframework.retry.backoff.BackOffPolicy; import org.springframework.retry.backoff.NoBackOffPolicy; @@ -58,6 +66,7 @@ import org.springframework.util.Assert; * @author Olga Maciaszek-Sharma * @since 2.2.6 */ +@SuppressWarnings({ "rawtypes", "unchecked" }) public class RetryableFeignBlockingLoadBalancerClient implements Client { private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class); @@ -70,12 +79,16 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client { private final LoadBalancerProperties properties; + private final LoadBalancerClientFactory loadBalancerClientFactory; + public RetryableFeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, - LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) { + LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties, + LoadBalancerClientFactory loadBalancerClientFactory) { this.delegate = delegate; this.loadBalancerClient = loadBalancerClient; this.loadBalancedRetryFactory = loadBalancedRetryFactory; this.properties = properties; + this.loadBalancerClientFactory = loadBalancerClientFactory; } @Override @@ -88,6 +101,11 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client { RetryTemplate retryTemplate = buildRetryTemplate(serviceId, request, retryPolicy); return retryTemplate.execute(context -> { Request feignRequest = null; + ServiceInstance retrievedServiceInstance = null; + Set supportedLifecycleProcessors = LoadBalancerLifecycleValidator + .getSupportedLifecycleProcessors( + loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class), + HttpRequestContext.class, ClientHttpResponse.class, ServiceInstance.class); // On retries the policy will choose the server and set it in the context // and extract the server and update the request being made if (context instanceof LoadBalancedRetryContext) { @@ -102,30 +120,38 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client { String hint = getHint(serviceId); DefaultRequest lbRequest = new DefaultRequest<>( new RetryableRequestContext(previousServiceInstance, request, hint)); - serviceInstance = loadBalancerClient.choose(serviceId, lbRequest); + supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest)); + retrievedServiceInstance = loadBalancerClient.choose(serviceId, lbRequest); if (LOG.isDebugEnabled()) { - LOG.debug(String.format("Selected service instance: %s", serviceInstance)); + LOG.debug(String.format("Selected service instance: %s", retrievedServiceInstance)); } - lbContext.setServiceInstance(serviceInstance); + lbContext.setServiceInstance(retrievedServiceInstance); } - if (serviceInstance != null) { + + if (retrievedServiceInstance == null) { + if (LOG.isWarnEnabled()) { + LOG.warn("Service instance was not resolved, executing the original request"); + } + org.springframework.cloud.client.loadbalancer.Response lbResponse = new DefaultResponse( + retrievedServiceInstance); + supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete( + new CompletionContext(CompletionContext.Status.DISCARD, + lbResponse))); + feignRequest = request; + } + else { if (LOG.isDebugEnabled()) { LOG.debug(String.format("Using service instance from LoadBalancedRetryContext: %s", - serviceInstance)); + retrievedServiceInstance)); } - String reconstructedUrl = loadBalancerClient.reconstructURI(serviceInstance, originalUri) + String reconstructedUrl = loadBalancerClient.reconstructURI(retrievedServiceInstance, originalUri) .toString(); feignRequest = Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(), request.charset(), request.requestTemplate()); } } - if (feignRequest == null) { - if (LOG.isWarnEnabled()) { - LOG.warn("Service instance was not resolved, executing the original request"); - } - feignRequest = request; - } - Response response = delegate.execute(feignRequest, options); + Response response = executeWithLoadBalancerLifecycleProcessing(options, feignRequest, + retrievedServiceInstance, retrievedServiceInstance != null, supportedLifecycleProcessors); int responseStatus = response.status(); if (retryPolicy != null && retryPolicy.retryableStatusCode(responseStatus)) { if (LOG.isDebugEnabled()) { @@ -143,6 +169,28 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client { }); } + private Response executeWithLoadBalancerLifecycleProcessing(Request.Options options, Request feignRequest, + ServiceInstance retrievedServiceInstance, boolean loadBalanced, + Set supportedLifecycleProcessors) throws IOException { + org.springframework.cloud.client.loadbalancer.Response lbResponse = new DefaultResponse( + retrievedServiceInstance); + try { + Response response = delegate.execute(feignRequest, options); + if (loadBalanced) { + supportedLifecycleProcessors.forEach(lifecycle -> lifecycle + .onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS, lbResponse, response))); + } + return response; + } + catch (Exception exception) { + if (loadBalanced) { + supportedLifecycleProcessors.forEach(lifecycle -> lifecycle + .onComplete(new CompletionContext<>(CompletionContext.Status.FAILED, exception, lbResponse))); + } + throw exception; + } + } + private RetryTemplate buildRetryTemplate(String serviceId, Request request, LoadBalancedRetryPolicy retryPolicy) { RetryTemplate retryTemplate = new RetryTemplate(); BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory.createBackOffPolicy(serviceId); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClientTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClientTests.java index b3cabd7b..89b686bb 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClientTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClientTests.java @@ -23,6 +23,8 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import feign.Client; import feign.Request; @@ -34,8 +36,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.CompletionContext; +import org.springframework.cloud.client.loadbalancer.DefaultRequestContext; +import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle; import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -66,8 +72,12 @@ class FeignBlockingLoadBalancerClientTests { private BlockingLoadBalancerClient loadBalancerClient = mock(BlockingLoadBalancerClient.class); + private final LoadBalancerClientFactory loadBalancerClientFactory = mock(LoadBalancerClientFactory.class); + + private final LoadBalancerProperties loadBalancerProperties = new LoadBalancerProperties(); + private FeignBlockingLoadBalancerClient feignBlockingLoadBalancerClient = new FeignBlockingLoadBalancerClient( - delegate, loadBalancerClient, new LoadBalancerProperties()); + delegate, loadBalancerClient, loadBalancerProperties, loadBalancerClientFactory); @Test void shouldExtractServiceIdFromRequestUrl() throws IOException { @@ -121,6 +131,35 @@ class FeignBlockingLoadBalancerClientTests { assertThat(new String(actualRequest.body())).isEqualTo("hello"); } + @Test + void shouldExecuteLoadBalancerLifecycleCallbacks() throws IOException { + Request request = testRequest(); + Request.Options options = new Request.Options(); + String url = "http://127.0.0.1/path"; + ServiceInstance serviceInstance = new DefaultServiceInstance("test-1", "test", "test-host", 8888, false); + when(loadBalancerClient.choose(eq("test"), any())).thenReturn(serviceInstance); + when(loadBalancerClient.reconstructURI(serviceInstance, URI.create("http://test/path"))) + .thenReturn(URI.create(url)); + String callbackTestHint = "callbackTestHint"; + loadBalancerProperties.getHint().put("test", callbackTestHint); + Map loadBalancerLifecycleBeans = new HashMap<>(); + loadBalancerLifecycleBeans.put("loadBalancerLifecycle", new TestLoadBalancerLifecycle()); + loadBalancerLifecycleBeans.put("anotherLoadBalancerLifecycle", new AnotherLoadBalancerLifecycle()); + when(loadBalancerClientFactory.getInstances("test", LoadBalancerLifecycle.class)) + .thenReturn(loadBalancerLifecycleBeans); + + Object actualResult = feignBlockingLoadBalancerClient.execute(request, options); + + Collection> lifecycleLogRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans + .get("loadBalancerLifecycle")).getStartLog().values(); + Collection> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) loadBalancerLifecycleBeans + .get("anotherLoadBalancerLifecycle")).getCompleteLog().values(); + assertThat(lifecycleLogRequests) + .extracting(lbRequest -> ((DefaultRequestContext) lbRequest.getContext()).getHint()) + .contains(callbackTestHint); + assertThat(anotherLifecycleLogRequests).extracting(CompletionContext::getClientResponse).contains(actualResult); + } + private Request testRequest() { return testRequest("test"); } @@ -137,4 +176,43 @@ class FeignBlockingLoadBalancerClientTests { } + protected static class TestLoadBalancerLifecycle implements LoadBalancerLifecycle { + + final ConcurrentHashMap> startLog = new ConcurrentHashMap<>(); + + final ConcurrentHashMap> completeLog = new ConcurrentHashMap<>(); + + @Override + public void onStart(org.springframework.cloud.client.loadbalancer.Request request) { + startLog.put(getName() + UUID.randomUUID(), request); + } + + @Override + public void onComplete(CompletionContext completionContext) { + completeLog.put(getName() + UUID.randomUUID(), completionContext); + } + + ConcurrentHashMap> getStartLog() { + return startLog; + } + + ConcurrentHashMap> getCompleteLog() { + return completeLog; + } + + protected String getName() { + return this.getClass().getSimpleName(); + } + + } + + protected static class AnotherLoadBalancerLifecycle extends TestLoadBalancerLifecycle { + + @Override + protected String getName() { + return this.getClass().getSimpleName(); + } + + } + } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClientTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClientTests.java index 4c27fded..8376a2d8 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClientTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClientTests.java @@ -23,6 +23,8 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import feign.Client; import feign.Request; @@ -35,10 +37,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.CompletionContext; +import org.springframework.cloud.client.loadbalancer.DefaultRequestContext; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle; import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; import org.springframework.cloud.loadbalancer.blocking.retry.BlockingLoadBalancedRetryPolicy; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -70,10 +76,12 @@ class RetryableFeignBlockingLoadBalancerClientTests { private BlockingLoadBalancerClient loadBalancerClient = mock(BlockingLoadBalancerClient.class); + private final LoadBalancerClientFactory loadBalancerClientFactory = mock(LoadBalancerClientFactory.class); + private LoadBalancerProperties properties = new LoadBalancerProperties(); private RetryableFeignBlockingLoadBalancerClient feignBlockingLoadBalancerClient = new RetryableFeignBlockingLoadBalancerClient( - delegate, loadBalancerClient, retryFactory, properties); + delegate, loadBalancerClient, retryFactory, properties, loadBalancerClientFactory); private ServiceInstance serviceInstance = new DefaultServiceInstance("test-a", "test", "testhost", 80, false); @@ -165,6 +173,37 @@ class RetryableFeignBlockingLoadBalancerClientTests { assertThat(new String(actualRequest.body())).isEqualTo("hello"); } + @Test + void shouldExecuteLoadBalancerLifecycleCallbacks() throws IOException { + Request request = testRequest(); + Request.Options options = new Request.Options(); + String url = "http://127.0.0.1/path"; + ServiceInstance serviceInstance = new DefaultServiceInstance("test-1", "test", "test-host", 8888, false); + when(loadBalancerClient.choose(eq("test"), any())).thenReturn(serviceInstance); + when(loadBalancerClient.reconstructURI(serviceInstance, URI.create("http://test/path"))) + .thenReturn(URI.create(url)); + Response response = testResponse(200); + when(delegate.execute(any(), any())).thenReturn(response); + String callbackTestHint = "callbackTestHint"; + properties.getHint().put("test", callbackTestHint); + Map loadBalancerLifecycleBeans = new HashMap<>(); + loadBalancerLifecycleBeans.put("loadBalancerLifecycle", new TestLoadBalancerLifecycle()); + loadBalancerLifecycleBeans.put("anotherLoadBalancerLifecycle", new AnotherLoadBalancerLifecycle()); + when(loadBalancerClientFactory.getInstances("test", LoadBalancerLifecycle.class)) + .thenReturn(loadBalancerLifecycleBeans); + + Object actualResult = feignBlockingLoadBalancerClient.execute(request, options); + + Collection> lifecycleLogRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans + .get("loadBalancerLifecycle")).getStartLog().values(); + Collection> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) loadBalancerLifecycleBeans + .get("anotherLoadBalancerLifecycle")).getCompleteLog().values(); + assertThat(lifecycleLogRequests) + .extracting(lbRequest -> ((DefaultRequestContext) lbRequest.getContext()).getHint()) + .contains(callbackTestHint); + assertThat(anotherLifecycleLogRequests).extracting(CompletionContext::getClientResponse).contains(actualResult); + } + private Request testRequest() { return testRequest("test"); } @@ -181,4 +220,44 @@ class RetryableFeignBlockingLoadBalancerClientTests { } + protected static class TestLoadBalancerLifecycle implements LoadBalancerLifecycle { + + final ConcurrentHashMap> startLog = new ConcurrentHashMap<>(); + + final ConcurrentHashMap> completeLog = new ConcurrentHashMap<>(); + + @Override + public void onStart(org.springframework.cloud.client.loadbalancer.Request request) { + startLog.put(getName() + UUID.randomUUID(), request); + } + + @Override + public void onComplete(CompletionContext completionContext) { + completeLog.put(getName() + UUID.randomUUID(), completionContext); + } + + ConcurrentHashMap> getStartLog() { + return startLog; + } + + ConcurrentHashMap> getCompleteLog() { + return completeLog; + } + + protected String getName() { + return this.getClass().getSimpleName(); + } + + } + + protected static class AnotherLoadBalancerLifecycle + extends FeignBlockingLoadBalancerClientTests.TestLoadBalancerLifecycle { + + @Override + protected String getName() { + return this.getClass().getSimpleName(); + } + + } + }