Merge remote-tracking branch 'origin/2.2.x'

# Conflicts:
#	docs/src/main/asciidoc/spring-cloud-commons.adoc
#	spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java
#	spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java
#	spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfiguration.java
#	spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java
#	spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java
#	spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionIntegrationTests.java
#	spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java
This commit is contained in:
Olga MaciaszekSharma
2021-03-12 14:49:19 +01:00
11 changed files with 273 additions and 22 deletions

View File

@@ -1085,6 +1085,53 @@ public class CustomLoadBalancerConfiguration {
}
----
=== Transform the load-balanced HTTP request
You can use the selected `ServiceInstance` to transform the load-balanced HTTP Request.
For `RestTemplate`, you need to implement and define `LoadBalancerRequestTransformer` as follows:
[source,java,indent=0]
----
@Bean
public LoadBalancerRequestTransformer transformer() {
return new LoadBalancerRequestTransformer() {
@Override
public HttpRequest transformRequest(HttpRequest request, ServiceInstance instance) {
return new HttpRequestWrapper(request) {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.putAll(super.getHeaders());
headers.add("X-InstanceId", instance.getInstanceId());
return headers;
}
};
}
};
}
----
For `WebClient`, you need to implement and define `LoadBalancerClientRequestTransformer` as follows:
[source,java,indent=0]
----
@Bean
public LoadBalancerClientRequestTransformer transformer() {
return new LoadBalancerClientRequestTransformer() {
@Override
public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) {
return ClientRequest.from(request)
.header("X-InstanceId", instance.getInstanceId())
.build();
}
};
}
----
If multiple transformers are defined, they are applied in the order in which Beans are defined.
Alternatively, you can use `LoadBalancerRequestTransformer.DEFAULT_ORDER` or `LoadBalancerClientRequestTransformer.DEFAULT_ORDER` to specify the order.
[[spring-cloud-loadbalancer-starter]]
=== Spring Cloud LoadBalancer Starter

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@@ -17,6 +17,7 @@
package org.springframework.cloud.client.loadbalancer.reactive;
import java.net.URI;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.client.ServiceInstance;
@@ -43,9 +44,11 @@ public final class ExchangeFilterFunctionUtils {
}
static ClientRequest buildClientRequest(ClientRequest request, ServiceInstance serviceInstance,
String instanceIdCookieName, boolean addServiceInstanceCookie) {
String instanceIdCookieName, boolean addServiceInstanceCookie,
List<LoadBalancerClientRequestTransformer> transformers) {
URI originalUrl = request.url();
return ClientRequest.create(request.method(), LoadBalancerUriTools.reconstructURI(serviceInstance, originalUrl))
ClientRequest clientRequest = ClientRequest
.create(request.method(), LoadBalancerUriTools.reconstructURI(serviceInstance, originalUrl))
.headers(headers -> headers.addAll(request.headers())).cookies(cookies -> {
cookies.addAll(request.cookies());
if (!(instanceIdCookieName == null || instanceIdCookieName.length() == 0)
@@ -53,6 +56,12 @@ public final class ExchangeFilterFunctionUtils {
cookies.add(instanceIdCookieName, serviceInstance.getInstanceId());
}
}).attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build();
if (transformers != null) {
for (LoadBalancerClientRequestTransformer transformer : transformers) {
clientRequest = transformer.transformRequest(clientRequest, serviceInstance);
}
}
return clientRequest;
}
static String serviceInstanceUnavailableMessage(String serviceId) {

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2020 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.reactive;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.core.annotation.Order;
import org.springframework.web.reactive.function.client.ClientRequest;
/**
* Allows applications to transform the load-balanced {@link ClientRequest} given the
* chosen {@link ServiceInstance}.
*
* @author Toshiaki Maki
*/
@Order(LoadBalancerClientRequestTransformer.DEFAULT_ORDER)
public interface LoadBalancerClientRequestTransformer {
/**
* Order for the load balancer request tranformer.
*/
int DEFAULT_ORDER = 0;
ClientRequest transformRequest(ClientRequest request, ServiceInstance instance);
}

View File

@@ -16,6 +16,10 @@
package org.springframework.cloud.client.loadbalancer.reactive;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -45,8 +49,10 @@ public class ReactorLoadBalancerClientAutoConfiguration {
matchIfMissing = true)
@Bean
public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory, LoadBalancerProperties properties) {
return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, properties);
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory, LoadBalancerProperties properties,
ObjectProvider<List<LoadBalancerClientRequestTransformer>> transformers) {
return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, properties,
transformers.getIfAvailable(Collections::emptyList));
}
@ConditionalOnMissingBean
@@ -54,8 +60,10 @@ public class ReactorLoadBalancerClientAutoConfiguration {
@Bean
public RetryableLoadBalancerExchangeFilterFunction retryableLoadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory, LoadBalancerProperties properties,
LoadBalancerRetryPolicy retryPolicy) {
return new RetryableLoadBalancerExchangeFilterFunction(retryPolicy, loadBalancerFactory, properties);
LoadBalancerRetryPolicy retryPolicy,
ObjectProvider<List<LoadBalancerClientRequestTransformer>> transformers) {
return new RetryableLoadBalancerExchangeFilterFunction(retryPolicy, loadBalancerFactory, properties,
transformers.getIfAvailable(Collections::emptyList));
}
@ConditionalOnMissingBean

View File

@@ -17,6 +17,8 @@
package org.springframework.cloud.client.loadbalancer.reactive;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
@@ -61,10 +63,25 @@ public class ReactorLoadBalancerExchangeFilterFunction implements LoadBalancedEx
private final LoadBalancerProperties properties;
private final List<LoadBalancerClientRequestTransformer> transformers;
/**
* @deprecated Deprecated in favor of
* {@link #ReactorLoadBalancerExchangeFilterFunction(ReactiveLoadBalancer.Factory, LoadBalancerProperties, List)}.
* @param loadBalancerFactory the loadbalancer factory
* @param properties the properties for SC LoadBalancer
*/
@Deprecated
public ReactorLoadBalancerExchangeFilterFunction(ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory,
LoadBalancerProperties properties) {
this(loadBalancerFactory, properties, Collections.emptyList());
}
public ReactorLoadBalancerExchangeFilterFunction(ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory,
LoadBalancerProperties properties, List<LoadBalancerClientRequestTransformer> transformers) {
this.loadBalancerFactory = loadBalancerFactory;
this.properties = properties;
this.transformers = transformers;
}
@Override
@@ -106,7 +123,7 @@ public class ReactorLoadBalancerExchangeFilterFunction implements LoadBalancedEx
LoadBalancerProperties.StickySession stickySessionProperties = properties.getStickySession();
ClientRequest newRequest = buildClientRequest(clientRequest, instance,
stickySessionProperties.getInstanceIdCookieName(),
stickySessionProperties.isAddServiceInstanceCookie());
stickySessionProperties.isAddServiceInstanceCookie(), transformers);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, lbResponse));
return next.exchange(newRequest)
.doOnError(throwable -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@@ -19,6 +19,7 @@ package org.springframework.cloud.client.loadbalancer.reactive;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeoutException;
@@ -66,8 +67,7 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced
private static final Log LOG = LogFactory.getLog(RetryableLoadBalancerExchangeFilterFunction.class);
private static final List<Class<? extends Throwable>> exceptions = Arrays.asList(IOException.class,
TimeoutException.class,
org.springframework.cloud.client.loadbalancer.reactive.RetryableStatusCodeException.class);
TimeoutException.class, RetryableStatusCodeException.class);
private final LoadBalancerRetryPolicy retryPolicy;
@@ -75,11 +75,25 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory;
private final List<LoadBalancerClientRequestTransformer> transformers;
/**
* @deprecated Deprecated in favor of
* {@link #RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy, ReactiveLoadBalancer.Factory, LoadBalancerProperties, List)}.
*/
@Deprecated
public RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy retryPolicy,
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory, LoadBalancerProperties properties) {
this(retryPolicy, loadBalancerFactory, properties, Collections.emptyList());
}
public RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy retryPolicy,
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory, LoadBalancerProperties properties,
List<LoadBalancerClientRequestTransformer> transformers) {
this.retryPolicy = retryPolicy;
this.loadBalancerFactory = loadBalancerFactory;
this.properties = properties;
this.transformers = transformers;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@@ -129,7 +143,7 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced
LoadBalancerProperties.StickySession stickySessionProperties = properties.getStickySession();
ClientRequest newRequest = buildClientRequest(clientRequest, instance,
stickySessionProperties.getInstanceIdCookieName(),
stickySessionProperties.isAddServiceInstanceCookie());
stickySessionProperties.isAddServiceInstanceCookie(), transformers);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, lbResponse));
return next.exchange(newRequest)
.doOnError(throwable -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2012-2020 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.reactive;
import java.net.URI;
import java.util.Arrays;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import reactor.core.publisher.Mono;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link LoadBalancerClientRequestTransformer}.
*
* @author Toshiaki Maki
*/
class LoadBalancerClientRequestTransformerTest {
private final LoadBalancerProperties properties = new LoadBalancerProperties();
private final LoadBalancerRetryPolicy policy = new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(
properties);
private final ReactiveLoadBalancer.Factory<ServiceInstance> factory = mock(ReactiveLoadBalancer.Factory.class);
private final ClientRequest clientRequest = mock(ClientRequest.class);
private final ClientResponse clientResponse = mock(ClientResponse.class);
private final ExchangeFunction next = mock(ExchangeFunction.class);
@BeforeEach
void setUp() {
when(factory.getInstance("testServiceId")).thenReturn(new TestReactiveLoadBalancer());
when(clientRequest.method()).thenReturn(HttpMethod.GET);
when(clientRequest.url()).thenReturn(URI.create("http://testServiceId"));
when(clientRequest.headers()).thenReturn(new HttpHeaders());
when(clientRequest.cookies()).thenReturn(new LinkedMultiValueMap<>());
when(next.exchange(any())).thenReturn(Mono.just(clientResponse));
when(clientResponse.statusCode()).thenReturn(HttpStatus.OK);
}
@Test
void transformReactorLoadBalancerExchangeFilterFunction() {
ArgumentCaptor<ClientRequest> captor = ArgumentCaptor.forClass(ClientRequest.class);
ReactorLoadBalancerExchangeFilterFunction filterFunction = new ReactorLoadBalancerExchangeFilterFunction(
factory, properties, Arrays.asList(new Transformer1(), new Transformer2()));
filterFunction.filter(clientRequest, next).subscribe();
verify(next).exchange(captor.capture());
HttpHeaders headers = captor.getValue().headers();
assertThat(headers.getFirst("X-ServiceId")).isEqualTo("testServiceId");
assertThat(headers.getFirst("X-InstanceId")).isEqualTo("testServiceId");
}
@Test
void transformRetryableLoadBalancerExchangeFilterFunction() {
ArgumentCaptor<ClientRequest> captor = ArgumentCaptor.forClass(ClientRequest.class);
RetryableLoadBalancerExchangeFilterFunction filterFunction = new RetryableLoadBalancerExchangeFilterFunction(
policy, factory, properties, Arrays.asList(new Transformer1(), new Transformer2()));
filterFunction.filter(clientRequest, next).subscribe();
verify(next).exchange(captor.capture());
HttpHeaders headers = captor.getValue().headers();
assertThat(headers.getFirst("X-ServiceId")).isEqualTo("testServiceId");
assertThat(headers.getFirst("X-InstanceId")).isEqualTo("testServiceId");
}
class Transformer1 implements LoadBalancerClientRequestTransformer {
@Override
public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) {
return ClientRequest.from(request).header("X-ServiceId", instance.getServiceId()).build();
}
}
class Transformer2 implements LoadBalancerClientRequestTransformer {
@Override
public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) {
return ClientRequest.from(request).header("X-InstanceId", instance.getInstanceId()).build();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.client.loadbalancer.reactive;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -57,7 +58,7 @@ class RetryableLoadBalancerExchangeFilterFunctionTests {
private final ReactiveLoadBalancer.Factory<ServiceInstance> factory = mock(ReactiveLoadBalancer.Factory.class);
private final RetryableLoadBalancerExchangeFilterFunction filterFunction = new RetryableLoadBalancerExchangeFilterFunction(
policy, factory, properties);
policy, factory, properties, Collections.emptyList());
private final ClientRequest clientRequest = mock(ClientRequest.class);
@@ -137,7 +138,7 @@ class RetryableLoadBalancerExchangeFilterFunctionTests {
properties.getRetry().getRetryableStatusCodes().add(404);
LoadBalancerRetryPolicy policy = new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(properties);
RetryableLoadBalancerExchangeFilterFunction filterFunction = new RetryableLoadBalancerExchangeFilterFunction(
policy, factory, properties);
policy, factory, properties, Collections.emptyList());
when(clientRequest.method()).thenReturn(HttpMethod.POST);
when(clientResponse.statusCode()).thenReturn(HttpStatus.NOT_FOUND);
when(next.exchange(any())).thenReturn(Mono.just(clientResponse));

View File

@@ -67,7 +67,7 @@ class CachingServiceInstanceListSupplierTests {
@Test
void shouldNotHangOnCachingWhenDelegateReturnsInfiniteStream() {
assertTimeoutPreemptively(ofMillis(500), () -> {
assertTimeoutPreemptively(ofMillis(1000), () -> {
blockingLoadBalancerClient.choose(SERVICE_ID);
});

View File

@@ -59,13 +59,13 @@ import static org.mockito.Mockito.when;
* @author Spencer Gibb
*/
@SpringBootTest
public class LoadBalancerTests {
class LoadBalancerTests {
@Autowired
private LoadBalancerClientFactory clientFactory;
@Test
public void roundRobbinLoadbalancerWorks() {
void roundRobbinLoadbalancerWorks() {
ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer = this.clientFactory.getInstance("myservice",
ReactiveLoadBalancer.class, ServiceInstance.class);
@@ -101,7 +101,7 @@ public class LoadBalancerTests {
}
@Test
public void emptyHosts() {
void emptyHosts() {
ResolvableType type = ResolvableType.forClassWithGenerics(ReactorLoadBalancer.class, ServiceInstance.class);
ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory.getInstance("unknownservice", type);
@@ -115,7 +115,7 @@ public class LoadBalancerTests {
}
@Test
public void staticConfigurationWorks() {
void staticConfigurationWorks() {
String serviceId = "test1";
RoundRobinLoadBalancer loadBalancer = new RoundRobinLoadBalancer(ServiceInstanceListSuppliers
.toProvider(serviceId, instance(serviceId, "1host", false), instance(serviceId, "2host-secure", true)),
@@ -129,7 +129,7 @@ public class LoadBalancerTests {
@SuppressWarnings("ConstantConditions")
@Test
public void canPassHintViaRequest() {
void canPassHintViaRequest() {
String serviceId = "test1";
RoundRobinLoadBalancer loadBalancer = new TestHintLoadBalancer(ServiceInstanceListSuppliers.toProvider(
serviceId, instance(serviceId, "1host", false), instance(serviceId, "2host-secure", true)), serviceId);
@@ -141,7 +141,7 @@ public class LoadBalancerTests {
}
@Test
public void selectedInstanceCallback() {
void selectedInstanceCallback() {
String serviceId = "test1";
ServiceInstance serviceInstance = instance(serviceId, "1host", false);
SameInstancePreferenceServiceInstanceListSupplier supplier = mock(