Add LoadBalancerClientRequestTransformer to 2.2.x (#925)

This commit is contained in:
Toshiaki Maki
2021-03-12 21:30:32 +09:00
committed by GitHub
parent 8bffa27e9a
commit eddaaac5cb
9 changed files with 285 additions and 14 deletions

View File

@@ -1080,6 +1080,53 @@ public class CustomLoadBalancerConfiguration {
TIP: This is also a replacement for Zookeeper `StickyRule`.
=== 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

@@ -17,7 +17,9 @@
package org.springframework.cloud.client.loadbalancer.reactive;
import java.net.URI;
import java.util.List;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
@@ -33,12 +35,21 @@ public final class ExchangeFilterFunctionUtils {
throw new IllegalStateException("Can't instantiate a utility class.");
}
static ClientRequest buildClientRequest(ClientRequest request, URI uri) {
return ClientRequest.create(request.method(), uri)
static ClientRequest buildClientRequest(ClientRequest request, URI uri,
ServiceInstance serviceInstance,
List<LoadBalancerClientRequestTransformer> transformers) {
ClientRequest clientRequest = ClientRequest.create(request.method(), uri)
.headers(headers -> headers.addAll(request.headers()))
.cookies(cookies -> cookies.addAll(request.cookies()))
.attributes(attributes -> attributes.putAll(request.attributes()))
.body(request.body()).build();
if (transformers != null) {
for (LoadBalancerClientRequestTransformer transformer : transformers) {
clientRequest = transformer.transformRequest(clientRequest,
serviceInstance);
}
}
return clientRequest;
}
}

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,11 +16,15 @@
package org.springframework.cloud.client.loadbalancer.reactive;
import java.util.Collections;
import java.util.List;
import javax.annotation.PostConstruct;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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;
@@ -60,8 +64,10 @@ public class ReactorLoadBalancerClientAutoConfiguration {
@ConditionalOnMissingBean
@Bean
public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory loadBalancerFactory) {
return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory);
ReactiveLoadBalancer.Factory loadBalancerFactory,
ObjectProvider<List<LoadBalancerClientRequestTransformer>> transformers) {
return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory,
transformers.getIfAvailable(Collections::emptyList));
}
}

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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -46,9 +48,24 @@ public class ReactorLoadBalancerExchangeFilterFunction implements ExchangeFilter
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory;
private final List<LoadBalancerClientRequestTransformer> transformers;
/**
* @deprecated Deprecated in favor of
* {@link #ReactorLoadBalancerExchangeFilterFunction(ReactiveLoadBalancer.Factory, List)}.
* @param loadBalancerFactory the loadbalancer factory
*/
@Deprecated
public ReactorLoadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
this(loadBalancerFactory, Collections.emptyList());
}
public ReactorLoadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory,
List<LoadBalancerClientRequestTransformer> transformers) {
this.loadBalancerFactory = loadBalancerFactory;
this.transformers = transformers;
}
@Override
@@ -82,7 +99,7 @@ public class ReactorLoadBalancerExchangeFilterFunction implements ExchangeFilter
serviceId, instance.getUri()));
}
ClientRequest newRequest = buildClientRequest(request,
reconstructURI(instance, originalUrl));
reconstructURI(instance, originalUrl), instance, transformers);
return next.exchange(newRequest);
});
}

View File

@@ -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.concurrent.TimeoutException;
@@ -66,22 +67,46 @@ public class RetryableLoadBalancerExchangeFilterFunction
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory;
private final List<LoadBalancerClientRequestTransformer> transformers;
/**
* @deprecated Deprecated in favor of
* {@link #RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy, ReactiveLoadBalancer.Factory, LoadBalancerRetryProperties, List)}.
* @param retryPolicy the retry policy
* @param loadBalancerFactory the loadbalancer factory
* @param retryProperties the retry properties
*/
@Deprecated
public RetryableLoadBalancerExchangeFilterFunction(
LoadBalancerRetryPolicy retryPolicy,
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory,
LoadBalancerRetryProperties retryProperties) {
this.retryPolicy = retryPolicy;
this.loadBalancerFactory = loadBalancerFactory;
this.retryProperties = retryProperties;
this(retryPolicy, loadBalancerFactory, retryProperties, Collections.emptyList());
}
/**
* @deprecated Deprecated in favor of
* {@link #RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy, ReactiveLoadBalancer.Factory, LoadBalancerRetryProperties, List)}.
* @param loadBalancerFactory the loadbalancer factory
* @param retryProperties the retry properties
*/
@Deprecated
public RetryableLoadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory,
LoadBalancerRetryProperties retryProperties) {
this.retryPolicy = new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(
retryProperties);
this(new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(retryProperties),
loadBalancerFactory, retryProperties);
}
public RetryableLoadBalancerExchangeFilterFunction(
LoadBalancerRetryPolicy retryPolicy,
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory,
LoadBalancerRetryProperties retryProperties,
List<LoadBalancerClientRequestTransformer> transformers) {
this.retryPolicy = retryPolicy;
this.loadBalancerFactory = loadBalancerFactory;
this.retryProperties = retryProperties;
this.transformers = transformers;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@@ -129,7 +154,7 @@ public class RetryableLoadBalancerExchangeFilterFunction
serviceId, instance.getUri()));
}
ClientRequest newRequest = buildClientRequest(clientRequest,
reconstructURI(instance, originalUrl));
reconstructURI(instance, originalUrl), instance, transformers);
return next.exchange(newRequest).map(clientResponse -> {
loadBalancerRetryContext.setClientResponse(clientResponse);
if (shouldRetrySameServiceInstance(loadBalancerRetryContext)) {

View File

@@ -0,0 +1,125 @@
/*
* 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.LoadBalancerRetryProperties;
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 LoadBalancerRetryProperties properties = new LoadBalancerRetryProperties();
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, 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

@@ -210,7 +210,7 @@ class RetryableLoadBalancerExchangeFilterFunctionIntegrationTests {
return new RetryableLoadBalancerExchangeFilterFunction(
new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(
properties),
factory, properties);
factory, properties, Collections.emptyList());
}
}

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;
@@ -58,7 +59,7 @@ class RetryableLoadBalancerExchangeFilterFunctionTests {
ReactiveLoadBalancer.Factory.class);
private final RetryableLoadBalancerExchangeFilterFunction filterFunction = new RetryableLoadBalancerExchangeFilterFunction(
policy, factory, properties);
policy, factory, properties, Collections.emptyList());
private final ClientRequest clientRequest = mock(ClientRequest.class);
@@ -145,7 +146,7 @@ class RetryableLoadBalancerExchangeFilterFunctionTests {
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));