Browse Source
* Provide non-reactive LB client implemenation to use with RestTemplate. Fixes gh-491. Fixes gh-553. * Add javadoc.pull/592/head
Olga Maciaszek-Sharma
5 years ago
committed by
GitHub
8 changed files with 413 additions and 7 deletions
@ -0,0 +1,93 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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(); |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue