Fix gh 491 gh 553 non reactive loadbalancer client (#590)
* Provide non-reactive LB client implemenation to use with RestTemplate. Fixes gh-491. Fixes gh-553. * Add javadoc.
This commit is contained in:
committed by
GitHub
parent
3f17c0d902
commit
c60df0f18e
@@ -336,7 +336,7 @@ For instance, Eureka's supported statuses are `UP`, `DOWN`, `OUT_OF_SERVICE`, an
|
||||
|
||||
=== Spring RestTemplate as a Load Balancer Client
|
||||
|
||||
`RestTemplate` can be automatically configured to use ribbon.
|
||||
`RestTemplate` can be automatically configured to use a Load-balancer client under the hood.
|
||||
To create a load-balanced `RestTemplate`, create a `RestTemplate` `@Bean` and use the `@LoadBalanced` qualifier, as shown in the following example:
|
||||
|
||||
[source,java,indent=0]
|
||||
@@ -369,6 +369,15 @@ The URI needs to use a virtual host name (that is, a service name, not a host na
|
||||
The Ribbon client is used to create a full physical address.
|
||||
See {githubroot}/spring-cloud-netflix/blob/master/spring-cloud-netflix-ribbon/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java[RibbonAutoConfiguration] for details of how the `RestTemplate` is set up.
|
||||
|
||||
IMPORTANT: In order to use a load-balanced `RestTemplate`, you need to have a load-balancer implementation in your classpath.
|
||||
The recommended implementation is `BlockingLoadBalancerClient`
|
||||
- add `org.springframework.cloud:spring-cloud-loadbalancer` in order to use it.
|
||||
The
|
||||
`RibbonLoadBalancerClient` also can be used, but it's now under maintenance and we do not recommend adding it to new projects.
|
||||
|
||||
WARNING: If you want to use `BlockingLoadBalancerClient`, make sure you do not have
|
||||
`RibbonLoadBalancerClient` in the project classpath, as for backward compatibility reasons, it will be used by default.
|
||||
|
||||
=== Spring WebClient as a Load Balancer Client
|
||||
|
||||
`WebClient` can be automatically configured to use a load-balancer client.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.client.loadbalancer.reactive;
|
||||
package org.springframework.cloud.client.loadbalancer;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
@@ -29,7 +29,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 2.2.0
|
||||
*/
|
||||
final class LoadBalancerUriTools {
|
||||
public final class LoadBalancerUriTools {
|
||||
|
||||
private LoadBalancerUriTools() {
|
||||
throw new IllegalStateException("Can't instantiate a utility class");
|
||||
@@ -82,7 +82,13 @@ final class LoadBalancerUriTools {
|
||||
return 80;
|
||||
}
|
||||
|
||||
static URI reconstructURI(ServiceInstance serviceInstance, URI original) {
|
||||
/**
|
||||
* Modifies the URI in order to redirect the request to a service instance of choice.
|
||||
* @param serviceInstance the {@link ServiceInstance} to redirect the request to.
|
||||
* @param original the {@link URI} from the original request
|
||||
* @return the modified {@link URI}
|
||||
*/
|
||||
public static URI reconstructURI(ServiceInstance serviceInstance, URI original) {
|
||||
if (serviceInstance == null) {
|
||||
throw new IllegalArgumentException("Service Instance cannot be null.");
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.client.loadbalancer.reactive;
|
||||
package org.springframework.cloud.client.loadbalancer;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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.loadbalancer.blocking.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.Response;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* The default {@link LoadBalancerClient} implementation.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class BlockingLoadBalancerClient implements LoadBalancerClient {
|
||||
|
||||
private final LoadBalancerClientFactory loadBalancerClientFactory;
|
||||
|
||||
public BlockingLoadBalancerClient(
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
this.loadBalancerClientFactory = loadBalancerClientFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
|
||||
throws IOException {
|
||||
ServiceInstance serviceInstance = choose(serviceId);
|
||||
if (serviceInstance == null) {
|
||||
throw new IllegalStateException("No instances available for " + serviceId);
|
||||
}
|
||||
return execute(serviceId, serviceInstance, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T execute(String serviceId, ServiceInstance serviceInstance,
|
||||
LoadBalancerRequest<T> request) throws IOException {
|
||||
try {
|
||||
return request.apply(serviceInstance);
|
||||
}
|
||||
catch (IOException iOException) {
|
||||
throw iOException;
|
||||
}
|
||||
catch (Exception exception) {
|
||||
ReflectionUtils.rethrowRuntimeException(exception);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI reconstructURI(ServiceInstance serviceInstance, URI original) {
|
||||
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceInstance choose(String serviceId) {
|
||||
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory
|
||||
.getInstance(serviceId);
|
||||
if (loadBalancer == null) {
|
||||
return null;
|
||||
}
|
||||
Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose())
|
||||
.block();
|
||||
if (loadBalancerResponse == null) {
|
||||
return null;
|
||||
}
|
||||
return loadBalancerResponse.getServer();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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.loadbalancer.config;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
|
||||
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* An autoconfiguration for {@link BlockingLoadBalancerClient}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@Configuration
|
||||
@LoadBalancerClients
|
||||
@AutoConfigureAfter(LoadBalancerAutoConfiguration.class)
|
||||
@AutoConfigureBefore({
|
||||
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.class,
|
||||
AsyncLoadBalancerAutoConfiguration.class })
|
||||
public class BlockingLoadBalancerClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(
|
||||
name = "org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient")
|
||||
public RibbonWarnLogger ribbonWarnLogger() {
|
||||
return new RibbonWarnLogger();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(LoadBalancerClientFactory.class)
|
||||
@ConditionalOnClass(RestTemplate.class)
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnMissingClass("org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient")
|
||||
public LoadBalancerClient loadBalancerClient(
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
return new BlockingLoadBalancerClient(loadBalancerClientFactory);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RibbonWarnLogger {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(RibbonWarnLogger.class);
|
||||
|
||||
@PostConstruct
|
||||
void logWarning() {
|
||||
if (LOG.isWarnEnabled()) {
|
||||
LOG.warn(
|
||||
"You already have RibbonLoadBalancerClient on your classpath. It will be used by default. To use "
|
||||
+ BlockingLoadBalancerClient.class.getSimpleName()
|
||||
+ " remove spring-cloud-starter-netflix-ribbon from your project.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# AutoConfiguration
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration
|
||||
|
||||
org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration,\
|
||||
org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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.loadbalancer.blocking.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.DefaultResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.EmptyResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.Response;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
|
||||
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
|
||||
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* Tests for {@link BlockingLoadBalancerClient}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ExtendWith(SpringExtension.class)
|
||||
class BlockingLoadBalancerClientTests {
|
||||
|
||||
@Autowired
|
||||
private BlockingLoadBalancerClient loadBalancerClient;
|
||||
|
||||
@Autowired
|
||||
private SimpleDiscoveryProperties properties;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
properties.getInstances().put("myservice",
|
||||
Collections.singletonList(
|
||||
new SimpleDiscoveryProperties.SimpleServiceInstance(
|
||||
URI.create("https://test.example:9999"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void correctServiceInstanceChosen() {
|
||||
ServiceInstance serviceInstance = loadBalancerClient.choose("myservice");
|
||||
assertThat(serviceInstance.getHost()).isEqualTo("test.example");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullReturnedIfInstanceMissing() {
|
||||
ServiceInstance serviceInstance = loadBalancerClient.choose("unknownservice");
|
||||
assertThat(serviceInstance).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestExecutedAgainstCorrectInstance() throws IOException {
|
||||
final String result = "result";
|
||||
Object actualResult = loadBalancerClient.execute("myservice",
|
||||
(LoadBalancerRequest<Object>) instance -> {
|
||||
assertThat(instance.getHost()).isEqualTo("test.example");
|
||||
return result;
|
||||
});
|
||||
assertThat(actualResult).isEqualTo(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionThrownIfInstanceNotAvailableForRequestExecution() throws IOException {
|
||||
try {
|
||||
final String result = "result";
|
||||
Object actualResult = loadBalancerClient.execute("unknownservice",
|
||||
(LoadBalancerRequest<Object>) instance -> result);
|
||||
assertThat(actualResult).isEqualTo(result);
|
||||
fail("Should have thrown exception.");
|
||||
}
|
||||
catch (Exception exception) {
|
||||
assertThat(exception).isNotNull();
|
||||
assertThat(exception).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(exception).hasMessage("No instances available for unknownservice");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionRethrownAsRuntime() {
|
||||
try {
|
||||
loadBalancerClient.execute("myservice", instance -> {
|
||||
assertThat(instance.getHost()).isEqualTo("test.example");
|
||||
throw new Exception("Should throw exception.");
|
||||
});
|
||||
fail("Should have thrown exception.");
|
||||
}
|
||||
catch (Exception exception) {
|
||||
assertThat(exception).isNotNull();
|
||||
assertThat(exception).isInstanceOf(RuntimeException.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void IOExceptionRethrown() {
|
||||
try {
|
||||
loadBalancerClient.execute("myservice", instance -> {
|
||||
assertThat(instance.getHost()).isEqualTo("test.example");
|
||||
throw new IOException("Should throw IO exception.");
|
||||
});
|
||||
fail("Should have thrown exception.");
|
||||
}
|
||||
catch (Exception exception) {
|
||||
assertThat(exception).isNotNull();
|
||||
assertThat(exception).isInstanceOf(IOException.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@LoadBalancerClients({
|
||||
@org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient(
|
||||
name = "myservice", configuration = MyServiceConfig.class),
|
||||
@org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient(
|
||||
name = "unknownservice",
|
||||
configuration = UnknownServiceConfig.class) })
|
||||
protected static class Config {
|
||||
|
||||
}
|
||||
|
||||
protected static class MyServiceConfig {
|
||||
|
||||
@Bean
|
||||
ReactorLoadBalancer<ServiceInstance> reactiveLoadBalancer(
|
||||
DiscoveryClient discoveryClient) {
|
||||
return new DiscoveryClientBasedReactiveLoadBalancer("myservice",
|
||||
discoveryClient);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected static class UnknownServiceConfig {
|
||||
|
||||
@Bean
|
||||
ReactorLoadBalancer<ServiceInstance> reactiveLoadBalancer(
|
||||
DiscoveryClient discoveryClient) {
|
||||
return new DiscoveryClientBasedReactiveLoadBalancer("unknownservice",
|
||||
discoveryClient);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DiscoveryClientBasedReactiveLoadBalancer
|
||||
implements ReactorServiceInstanceLoadBalancer {
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
private final String serviceId;
|
||||
|
||||
private final DiscoveryClient discoveryClient;
|
||||
|
||||
DiscoveryClientBasedReactiveLoadBalancer(String serviceId,
|
||||
DiscoveryClient discoveryClient) {
|
||||
this.serviceId = serviceId;
|
||||
this.discoveryClient = discoveryClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Response<ServiceInstance>> choose() {
|
||||
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
|
||||
if (instances.size() == 0) {
|
||||
return Mono.just(new EmptyResponse());
|
||||
}
|
||||
int instanceIdx = this.random.nextInt(instances.size());
|
||||
return Mono.just(new DefaultResponse(instances.get(instanceIdx)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Response<ServiceInstance>> choose(Request request) {
|
||||
return choose();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user