Compare commits

...

2 Commits

Author SHA1 Message Date
Spencer Gibb 1de7b0cfdb
Adds UrlHealthIndicator based on WebClient 5 years ago
Spencer Gibb 388ea7b8e9
Initial support for DiscoveryClientEndpoint and LoadBalancerEndpoint 5 years ago
  1. 50
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/endpoint/DiscoveryClientEndpoint.java
  2. 42
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/endpoint/DiscoveryClientEndpointAutoConfiguration.java
  3. 7
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryProperties.java
  4. 25
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancerAutoConfiguration.java
  5. 60
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/endpoint/LoadBalancerEndpoint.java
  6. 64
      spring-cloud-commons/src/main/java/org/springframework/cloud/commons/util/UrlHealthIndicator.java
  7. 1
      spring-cloud-commons/src/main/resources/META-INF/spring.factories
  8. 82
      spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/endpoint/DiscoveryClientEndpointTests.java
  9. 126
      spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/endpoint/LoadBalancerEndpointTests.java
  10. 27
      spring-cloud-commons/src/test/resources/application-lbendpoint.yml

50
spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/endpoint/DiscoveryClientEndpoint.java

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
/*
* Copyright 2013-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.discovery.endpoint;
import java.util.List;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
@Endpoint(id = "discovery")
public class DiscoveryClientEndpoint {
private final ReactiveDiscoveryClient discoveryClient;
public DiscoveryClientEndpoint(ReactiveDiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
@ReadOperation
public Mono<List<String>> services() {
return this.discoveryClient.getServices().collectList();
}
@ReadOperation
public Mono<List<ServiceInstance>> instances(@Selector String serviceId) {
Flux<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId);
return instances.collectList();
}
}

42
spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/endpoint/DiscoveryClientEndpointAutoConfiguration.java

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
/*
* Copyright 2013-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.discovery.endpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClientAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Endpoint.class)
@AutoConfigureAfter(CompositeDiscoveryClientAutoConfiguration.class)
public class DiscoveryClientEndpointAutoConfiguration {
@Bean
@ConditionalOnAvailableEndpoint
@ConditionalOnBean(ReactiveDiscoveryClient.class)
public DiscoveryClientEndpoint discoveryClientEndpoint(
ReactiveDiscoveryClient discoveryClient) {
return new DiscoveryClientEndpoint(discoveryClient);
}
}

7
spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryProperties.java

@ -31,8 +31,6 @@ import org.springframework.cloud.client.ServiceInstance; @@ -31,8 +31,6 @@ import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import static java.util.Collections.emptyList;
/**
* Properties to hold the details of a {@link ReactiveDiscoveryClient} service instance
* for a given service. It also holds the user-configurable order that will be used to
@ -57,7 +55,10 @@ public class SimpleReactiveDiscoveryProperties { @@ -57,7 +55,10 @@ public class SimpleReactiveDiscoveryProperties {
private int order = DiscoveryClient.DEFAULT_ORDER;
public Flux<ServiceInstance> getInstances(String service) {
return Flux.fromIterable(instances.getOrDefault(service, emptyList()));
if (instances.containsKey(service)) {
return Flux.fromIterable(instances.get(service));
}
return Flux.empty();
}
Map<String, List<SimpleServiceInstance>> getInstances() {

25
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancerAutoConfiguration.java

@ -16,32 +16,45 @@ @@ -16,32 +16,45 @@
package org.springframework.cloud.client.loadbalancer.reactive;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.reactive.endpoint.LoadBalancerEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @deprecated in favour of {@link ReactorLoadBalancerClientAutoConfiguration}
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebClient.class)
@ConditionalOnBean(LoadBalancerClient.class)
@AutoConfigureAfter(ReactorLoadBalancerClientAutoConfiguration.class)
@ConditionalOnMissingBean(ReactorLoadBalancerExchangeFilterFunction.class)
@Deprecated
public class ReactiveLoadBalancerAutoConfiguration {
@Bean
@ConditionalOnClass(
name = "org.springframework.web.reactive.function.client.WebClient")
@ConditionalOnMissingBean(ReactorLoadBalancerExchangeFilterFunction.class)
@ConditionalOnBean(LoadBalancerClient.class)
@Deprecated
public LoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction(
LoadBalancerClient client) {
return new LoadBalancerExchangeFilterFunction(client);
}
@Bean
@ConditionalOnAvailableEndpoint
@ConditionalOnClass(
name = "org.springframework.boot.actuate.endpoint.annotation.Endpoint")
@ConditionalOnBean(ReactiveLoadBalancer.Factory.class)
public LoadBalancerEndpoint loadBalancerEndpoint(
ObjectProvider<ReactiveLoadBalancer.Factory<ServiceInstance>> clientFactory) {
return new LoadBalancerEndpoint(clientFactory);
}
}

60
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/endpoint/LoadBalancerEndpoint.java

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
/*
* Copyright 2013-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.endpoint;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer.Factory;
@Endpoint(id = "loadbalancer")
public class LoadBalancerEndpoint {
private final ObjectProvider<Factory<ServiceInstance>> clientFactory;
public LoadBalancerEndpoint(ObjectProvider<Factory<ServiceInstance>> clientFactory) {
this.clientFactory = clientFactory;
}
@ReadOperation
public Mono<ServiceInstance> choose(@Selector String serviceId,
@Selector String operation) {
if (!"choose".equalsIgnoreCase(operation)) {
return Mono.error(
new IllegalArgumentException("Unknown operation: " + operation));
}
Factory<ServiceInstance> factory = this.clientFactory.getIfAvailable();
if (factory == null) {
return Mono.empty();
}
ReactiveLoadBalancer<ServiceInstance> loadBalancer = factory
.getInstance(serviceId);
return Mono.from(loadBalancer.choose()).flatMap(response -> {
if (response.hasServer()) {
return Mono.just(response.getServer());
}
return Mono.empty();
});
}
}

64
spring-cloud-commons/src/main/java/org/springframework/cloud/commons/util/UrlHealthIndicator.java

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
/*
* Copyright 2013-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.commons.util;
import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@link ReactiveHealthIndicator} that checks the configured {@link WebClient} for the
* Spring Boot Health status format.
* @author Spencer Gibb
* @author Fabrizio Di Napoli
*/
public class UrlHealthIndicator extends AbstractReactiveHealthIndicator {
private final WebClient webClient;
public UrlHealthIndicator(WebClient webClient) {
this.webClient = webClient;
}
public UrlHealthIndicator(WebClient.Builder builder, String baseUrl) {
this.webClient = builder.baseUrl(baseUrl).build();
}
@Override
protected Mono<Health> doHealthCheck(Health.Builder builder) {
if (this.webClient == null) {
return Mono.just(builder.up().build());
}
return webClient.get().retrieve().bodyToMono(Map.class).map(map -> {
Object status = map.get("status");
if (status instanceof String) {
return builder.status(status.toString()).build();
}
else {
return builder.unknown()
.withDetail("warning", "no status field in response").build();
}
});
}
}

1
spring-cloud-commons/src/main/resources/META-INF/spring.factories

@ -4,6 +4,7 @@ org.springframework.cloud.client.CommonsClientAutoConfiguration,\ @@ -4,6 +4,7 @@ org.springframework.cloud.client.CommonsClientAutoConfiguration,\
org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration,\
org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.discovery.composite.reactive.ReactiveCompositeDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.discovery.endpoint.DiscoveryClientEndpointAutoConfiguration,\
org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration,\

82
spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/endpoint/DiscoveryClientEndpointTests.java

@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
/*
* Copyright 2013-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.discovery.endpoint;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
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.discovery.simple.reactive.SimpleReactiveDiscoveryProperties.SimpleServiceInstance;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = { "management.endpoints.web.exposure.include=discovery" },
webEnvironment = RANDOM_PORT)
@ActiveProfiles("lbendpoint")
public class DiscoveryClientEndpointTests {
@Autowired
private WebTestClient client;
@Test
public void servicesWorks() {
client.get().uri("/actuator/discovery").exchange().expectBody(List.class)
.consumeWith(result -> {
List<String> body = result.getResponseBody();
assertThat(body).contains("myservice", "anotherservice",
"thirdservice");
});
}
@Test
public void instancesWorks() {
client.get().uri("/actuator/discovery/myservice").exchange()
.expectBodyList(SimpleServiceInstance.class).consumeWith(result -> {
List body = result.getResponseBody();
assertThat(body).isNotEmpty();
});
}
@Test
public void noInstancesWorks() {
client.get().uri("/actuator/discovery/unknownservice").exchange()
.expectBodyList(SimpleServiceInstance.class).consumeWith(result -> {
List body = result.getResponseBody();
assertThat(body).isEmpty();
});
}
@SpringBootConfiguration
@EnableAutoConfiguration
protected static class TestConfiguration {
}
}

126
spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/endpoint/LoadBalancerEndpointTests.java

@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
/*
* Copyright 2013-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.endpoint;
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
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.ReactiveDiscoveryClient;
import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryProperties.SimpleServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.CompletionContext;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = { "management.endpoints.web.exposure.include=loadbalancer" },
webEnvironment = RANDOM_PORT)
@ActiveProfiles("lbendpoint")
public class LoadBalancerEndpointTests {
private static final Random random = new Random();
@Autowired
private WebTestClient client;
@Test
public void chooseWorks() {
client.get().uri("/actuator/loadbalancer/myservice/choose").exchange()
.expectBody(SimpleServiceInstance.class).consumeWith(result -> {
ServiceInstance body = result.getResponseBody();
assertThat(body).isNotNull();
assertThat(body.getHost()).isNotEmpty();
});
}
@Test
public void chooseWorksWithoutInstances() {
client.get().uri("/actuator/loadbalancer/unknownservice/choose").exchange()
.expectBody(SimpleServiceInstance.class).consumeWith(result -> {
ServiceInstance body = result.getResponseBody();
assertThat(body).isNull();
// TODO: web response 404?
});
}
static Mono<Response<ServiceInstance>> choose(ReactiveDiscoveryClient discoveryClient,
String serviceId) {
return discoveryClient.getInstances(serviceId).collectList().map(instances -> {
if (instances.isEmpty()) {
return new InstanceResponse(null);
}
int idx = random.nextInt(instances.size());
ServiceInstance instance = instances.get(idx);
return new InstanceResponse(instance);
});
}
private static final class InstanceResponse implements Response<ServiceInstance> {
private final ServiceInstance instance;
private InstanceResponse(ServiceInstance instance) {
this.instance = instance;
}
@Override
public boolean hasServer() {
return this.instance != null;
}
@Override
public ServiceInstance getServer() {
return this.instance;
}
@Override
public void onComplete(CompletionContext completionContext) {
}
}
@SpringBootConfiguration
@EnableAutoConfiguration
protected static class TestConfiguration {
@Bean
public ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory(
ReactiveDiscoveryClient discoveryClient) {
return serviceId -> (ReactiveLoadBalancer<ServiceInstance>) request -> choose(
discoveryClient, serviceId);
};
}
}

27
spring-cloud-commons/src/test/resources/application-lbendpoint.yml

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
spring:
cloud:
discovery:
client:
simple:
instances:
myservice:
- service-id: myservice
uri: http://ahost:8080
- service-id: myservice
uri: http://chost:8081
- service-id: myservice
uri: https://bhostsecure:8082
anotherservice:
- service-id: myservice
uri: http://dhost:8180
- service-id: myservice
uri: https://fhost:8181
- service-id: myservice
uri: http://ehost:8182
thirdservice:
- service-id: myservice
uri: http://ghost:8280
- service-id: myservice
uri: http://hhost:8281
- service-id: myservice
uri: http://ihost:8282
Loading…
Cancel
Save