diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc
index 568246b9..96ba462c 100644
--- a/docs/src/main/asciidoc/spring-cloud-commons.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc
@@ -241,10 +241,14 @@ Patterns such as service discovery, load balancing, and circuit breakers lend th
=== @EnableDiscoveryClient
Spring Cloud Commons provides the `@EnableDiscoveryClient` annotation.
-This looks for implementations of the `DiscoveryClient` interface with `META-INF/spring.factories`.
-Implementations of the Discovery Client add a configuration class to `spring.factories` under the `org.springframework.cloud.client.discovery.EnableDiscoveryClient` key.
+This looks for implementations of the `DiscoveryClient` and `ReactiveDiscoveryClient` interfaces with `META-INF/spring.factories`.
+Implementations of the discovery client add a configuration class to `spring.factories` under the `org.springframework.cloud.client.discovery.EnableDiscoveryClient` key.
Examples of `DiscoveryClient` implementations include https://cloud.spring.io/spring-cloud-netflix/[Spring Cloud Netflix Eureka], https://cloud.spring.io/spring-cloud-consul/[Spring Cloud Consul Discovery], and https://cloud.spring.io/spring-cloud-zookeeper/[Spring Cloud Zookeeper Discovery].
+Spring Cloud will provide both the blocking and reactive service discovery clients by default.
+You can disable the blocking and/or reactive clients easily by setting `spring.cloud.discovery.blocking.enabled=false` or `spring.cloud.discovery.reactive.enabled=false`.
+To completely disable service discovery you just need to set `spring.cloud.discovery.enabled=false`.
+
By default, implementations of `DiscoveryClient` auto-register the local Spring Boot server with the remote discovery server.
This behavior can be disabled by setting `autoRegister=false` in `@EnableDiscoveryClient`.
diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml
index 7a864ee1..875ca30f 100644
--- a/spring-cloud-commons/pom.xml
+++ b/spring-cloud-commons/pom.xml
@@ -163,5 +163,10 @@
spring-cloud-test-support
test
+
+ io.projectreactor
+ reactor-test
+ test
+
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java
index 0f890f16..d3de269f 100644
--- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java
@@ -47,6 +47,7 @@ import org.springframework.context.annotation.Configuration;
*
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
+ * @author Tim Ysewyn
*/
@Configuration
@AutoConfigureOrder(0)
@@ -57,6 +58,7 @@ public class CommonsClientAutoConfiguration {
@ConditionalOnClass(HealthIndicator.class)
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnDiscoveryEnabled
+ @ConditionalOnBlockingDiscoveryEnabled
protected static class DiscoveryLoadBalancerConfiguration {
@Bean
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnBlockingDiscoveryEnabled.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnBlockingDiscoveryEnabled.java
new file mode 100644
index 00000000..e026fa5d
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnBlockingDiscoveryEnabled.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2019-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.client;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+/**
+ * Provides a more succinct conditional
+ * spring.cloud.discovery.blocking.enabled.
+ *
+ * @author Tim Ysewyn
+ * @since 2.2.0
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@ConditionalOnProperty(value = "spring.cloud.discovery.blocking.enabled",
+ matchIfMissing = true)
+public @interface ConditionalOnBlockingDiscoveryEnabled {
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnReactiveDiscoveryEnabled.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnReactiveDiscoveryEnabled.java
new file mode 100644
index 00000000..9a7bacee
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ConditionalOnReactiveDiscoveryEnabled.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2019-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.client;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.web.reactive.function.client.WebClient;
+
+/**
+ * Provides a more succinct conditional
+ * spring.cloud.discovery.reactive.enabled. Also takes into account whether
+ * or not `WebClient` is on the classpath.
+ *
+ * @author Tim Ysewyn
+ * @since 2.2.0
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@ConditionalOnClass(WebClient.class)
+@ConditionalOnProperty(value = "spring.cloud.discovery.reactive.enabled",
+ matchIfMissing = true)
+public @interface ConditionalOnReactiveDiscoveryEnabled {
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfiguration.java
new file mode 100644
index 00000000..44ca9839
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfiguration.java
@@ -0,0 +1,74 @@
+/*
+ * 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.client;
+
+import java.util.Collection;
+
+import org.springframework.boot.actuate.health.HealthIndicator;
+import org.springframework.boot.autoconfigure.AutoConfigureOrder;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.client.actuator.HasFeatures;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties;
+import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryCompositeHealthContributor;
+import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryHealthIndicator;
+import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} for reactive Spring Cloud Commons
+ * Client.
+ *
+ * @author Tim Ysewyn
+ * @since 2.2.0
+ */
+@Configuration
+@AutoConfigureOrder(0)
+public class ReactiveCommonsClientAutoConfiguration {
+
+ @Configuration
+ @EnableConfigurationProperties(DiscoveryClientHealthIndicatorProperties.class)
+ @ConditionalOnClass(HealthIndicator.class)
+ @ConditionalOnBean(ReactiveDiscoveryClient.class)
+ @ConditionalOnDiscoveryEnabled
+ @ConditionalOnReactiveDiscoveryEnabled
+ protected static class ReactiveDiscoveryLoadBalancerConfiguration {
+
+ @Bean
+ @ConditionalOnProperty(
+ value = "spring.cloud.discovery.client.composite-indicator.enabled",
+ matchIfMissing = true)
+ @ConditionalOnBean({ ReactiveDiscoveryHealthIndicator.class })
+ public ReactiveDiscoveryCompositeHealthContributor reactiveDiscoveryClients(
+ Collection indicators) {
+ return new ReactiveDiscoveryCompositeHealthContributor(indicators);
+ }
+
+ @Bean
+ public HasFeatures reactiveCommonsFeatures() {
+ return HasFeatures.abstractFeatures(ReactiveDiscoveryClient.class,
+ ReactiveLoadBalancer.class);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/ReactiveDiscoveryClient.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/ReactiveDiscoveryClient.java
new file mode 100644
index 00000000..dbbd1ccb
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/ReactiveDiscoveryClient.java
@@ -0,0 +1,64 @@
+/*
+ * 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.client.discovery;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.core.Ordered;
+
+/**
+ * Represents read operations commonly available to discovery services such as Netflix
+ * Eureka or consul.io.
+ *
+ * @author Tim Ysewyn
+ */
+public interface ReactiveDiscoveryClient extends Ordered {
+
+ /**
+ * Default order of the discovery client.
+ */
+ int DEFAULT_ORDER = 0;
+
+ /**
+ * A human-readable description of the implementation, used in HealthIndicator.
+ * @return The description.
+ */
+ String description();
+
+ /**
+ * Gets all ServiceInstances associated with a particular serviceId.
+ * @param serviceId The serviceId to query.
+ * @return A List of ServiceInstance.
+ */
+ Flux getInstances(String serviceId);
+
+ /**
+ * @return All known service IDs.
+ */
+ Flux getServices();
+
+ /**
+ * Default implementation for getting order of discovery clients.
+ * @return order
+ */
+ @Override
+ default int getOrder() {
+ return DEFAULT_ORDER;
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClient.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClient.java
new file mode 100644
index 00000000..01889c09
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClient.java
@@ -0,0 +1,74 @@
+/*
+ * 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.client.discovery.composite.reactive;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
+
+/**
+ * A {@link ReactiveDiscoveryClient} that is composed of other discovery clients and
+ * delegates calls to each of them in order.
+ *
+ * @author Tim Ysewyn
+ */
+public class ReactiveCompositeDiscoveryClient implements ReactiveDiscoveryClient {
+
+ private final List discoveryClients;
+
+ public ReactiveCompositeDiscoveryClient(
+ List discoveryClients) {
+ AnnotationAwareOrderComparator.sort(discoveryClients);
+ this.discoveryClients = discoveryClients;
+ }
+
+ @Override
+ public String description() {
+ return "Composite Reactive Discovery Client";
+ }
+
+ @Override
+ public Flux getInstances(String serviceId) {
+ if (discoveryClients == null || discoveryClients.isEmpty()) {
+ return Flux.empty();
+ }
+ List> serviceInstances = new ArrayList<>();
+ for (ReactiveDiscoveryClient discoveryClient : discoveryClients) {
+ serviceInstances.add(discoveryClient.getInstances(serviceId));
+ }
+ return Flux.first(serviceInstances);
+ }
+
+ @Override
+ public Flux getServices() {
+ if (discoveryClients == null || discoveryClients.isEmpty()) {
+ return Flux.empty();
+ }
+ return Flux.fromIterable(discoveryClients)
+ .flatMap(ReactiveDiscoveryClient::getServices);
+ }
+
+ List getDiscoveryClients() {
+ return discoveryClients;
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfiguration.java
new file mode 100644
index 00000000..b6a0c0ac
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfiguration.java
@@ -0,0 +1,49 @@
+/*
+ * 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.client.discovery.composite.reactive;
+
+import java.util.List;
+
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
+import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+/**
+ * Auto-configuration for reactive composite discovery client.
+ *
+ * @author Tim Ysewyn
+ * @since 2.2.0
+ */
+@Configuration
+@ConditionalOnDiscoveryEnabled
+@ConditionalOnReactiveDiscoveryEnabled
+@AutoConfigureBefore(SimpleReactiveDiscoveryClientAutoConfiguration.class)
+public class ReactiveCompositeDiscoveryClientAutoConfiguration {
+
+ @Bean
+ @Primary
+ public ReactiveCompositeDiscoveryClient reactiveCompositeDiscoveryClient(
+ List discoveryClients) {
+ return new ReactiveCompositeDiscoveryClient(discoveryClients);
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicator.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicator.java
new file mode 100644
index 00000000..b289187a
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicator.java
@@ -0,0 +1,114 @@
+/*
+ * 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.client.discovery.health.reactive;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import reactor.core.publisher.Mono;
+
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.Status;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent;
+import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties;
+import org.springframework.context.ApplicationListener;
+import org.springframework.core.Ordered;
+
+import static java.util.Collections.emptyList;
+
+/**
+ * A health indicator which indicates whether or not the discovery client has been
+ * initialized.
+ *
+ * @author Tim Ysewyn
+ */
+public class ReactiveDiscoveryClientHealthIndicator
+ implements ReactiveDiscoveryHealthIndicator, Ordered,
+ ApplicationListener> {
+
+ private final ReactiveDiscoveryClient discoveryClient;
+
+ private final DiscoveryClientHealthIndicatorProperties properties;
+
+ private final Log log = LogFactory
+ .getLog(ReactiveDiscoveryClientHealthIndicator.class);
+
+ private AtomicBoolean discoveryInitialized = new AtomicBoolean(false);
+
+ private int order = Ordered.HIGHEST_PRECEDENCE;
+
+ public ReactiveDiscoveryClientHealthIndicator(ReactiveDiscoveryClient discoveryClient,
+ DiscoveryClientHealthIndicatorProperties properties) {
+ this.discoveryClient = discoveryClient;
+ this.properties = properties;
+ }
+
+ @Override
+ public void onApplicationEvent(InstanceRegisteredEvent> event) {
+ if (this.discoveryInitialized.compareAndSet(false, true)) {
+ this.log.debug("Discovery Client has been initialized");
+ }
+ }
+
+ @Override
+ public Mono health() {
+ if (this.discoveryInitialized.get()) {
+ return doHealthCheck();
+ }
+ else {
+ return Mono.just(Health.status(new Status(Status.UNKNOWN.getCode(),
+ "Discovery Client not initialized")).build());
+ }
+ }
+
+ private Mono doHealthCheck() {
+ // @formatter:off
+ return Mono.justOrEmpty(this.discoveryClient)
+ .flatMapMany(ReactiveDiscoveryClient::getServices)
+ .collectList()
+ .defaultIfEmpty(emptyList())
+ .map(services -> {
+ ReactiveDiscoveryClient client = this.discoveryClient;
+ String description = (this.properties.isIncludeDescription())
+ ? client.description() : "";
+ return Health.status(new Status("UP", description))
+ .withDetail("services", services).build();
+ })
+ .onErrorResume(exception -> {
+ this.log.error("Error", exception);
+ return Mono.just(Health.down().withException(exception).build());
+ });
+ // @formatter:on
+ }
+
+ @Override
+ public String getName() {
+ return discoveryClient.description();
+ }
+
+ @Override
+ public int getOrder() {
+ return this.order;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributor.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributor.java
new file mode 100644
index 00000000..202dcbd9
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributor.java
@@ -0,0 +1,80 @@
+/*
+ * 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.client.discovery.health.reactive;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor;
+import org.springframework.boot.actuate.health.NamedContributor;
+import org.springframework.boot.actuate.health.ReactiveHealthContributor;
+import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
+import org.springframework.util.Assert;
+
+/**
+ * A composite health contributor specific to a reactive discovery client implementation.
+ *
+ * @author Tim Ysewyn
+ */
+public class ReactiveDiscoveryCompositeHealthContributor
+ implements CompositeReactiveHealthContributor {
+
+ private Map indicators;
+
+ public ReactiveDiscoveryCompositeHealthContributor(
+ Collection indicators) {
+ Assert.notNull(indicators, "'indicators' must not be null");
+ this.indicators = indicators.stream().collect(Collectors
+ .toMap(ReactiveDiscoveryHealthIndicator::getName, Function.identity()));
+ }
+
+ @Override
+ public ReactiveHealthContributor getContributor(String name) {
+ return asHealthIndicator(indicators.get(name));
+ }
+
+ @Override
+ public Iterator> iterator() {
+ return indicators.values().stream().map(this::asNamedContributor).iterator();
+ }
+
+ private NamedContributor asNamedContributor(
+ ReactiveDiscoveryHealthIndicator indicator) {
+ return new NamedContributor() {
+
+ @Override
+ public String getName() {
+ return indicator.getName();
+ }
+
+ @Override
+ public ReactiveHealthContributor getContributor() {
+ return asHealthIndicator(indicator);
+ }
+
+ };
+ }
+
+ private ReactiveHealthIndicator asHealthIndicator(
+ ReactiveDiscoveryHealthIndicator indicator) {
+ return (indicator != null) ? indicator::health : null;
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryHealthIndicator.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryHealthIndicator.java
new file mode 100644
index 00000000..6b5f535d
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryHealthIndicator.java
@@ -0,0 +1,43 @@
+/*
+ * 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.client.discovery.health.reactive;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.boot.actuate.health.Health;
+
+/**
+ * A health indicator interface specific to a reactive discovery client implementation.
+ *
+ * @author Tim Ysewyn
+ */
+public interface ReactiveDiscoveryHealthIndicator {
+
+ /**
+ * Provide the name of health indicator.
+ * @return a {@link String} that provides the name of health indicator, usually the
+ * name of the implementation.
+ */
+ String getName();
+
+ /**
+ * Provide the indicator of health.
+ * @return a {@link Mono} that provides the {@link Health}
+ */
+ Mono health();
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/SimpleDiscoveryClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/SimpleDiscoveryClientAutoConfiguration.java
index 0062e2e6..1fb375e8 100644
--- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/SimpleDiscoveryClientAutoConfiguration.java
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/SimpleDiscoveryClientAutoConfiguration.java
@@ -23,10 +23,10 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.context.WebServerInitializedEvent;
+import org.springframework.cloud.client.CommonsClientAutoConfiguration;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration;
import org.springframework.cloud.commons.util.InetUtils;
-import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -38,18 +38,15 @@ import org.springframework.core.annotation.Order;
*
* @author Biju Kunjummen
*/
-
@Configuration
-@AutoConfigureBefore(NoopDiscoveryClientAutoConfiguration.class)
+@AutoConfigureBefore({ NoopDiscoveryClientAutoConfiguration.class,
+ CommonsClientAutoConfiguration.class })
public class SimpleDiscoveryClientAutoConfiguration
implements ApplicationListener {
@Autowired(required = false)
private ServerProperties server;
- @Autowired
- private ApplicationContext context;
-
@Value("${spring.application.name:application}")
private String serviceId;
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClient.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClient.java
new file mode 100644
index 00000000..b4c28af4
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClient.java
@@ -0,0 +1,59 @@
+/*
+ * 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.client.discovery.simple.reactive;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+
+/**
+ * A {@link ReactiveDiscoveryClient} that will use the properties file as a source of
+ * service instances.
+ *
+ * @author Tim Ysewyn
+ */
+public class SimpleReactiveDiscoveryClient implements ReactiveDiscoveryClient {
+
+ private SimpleReactiveDiscoveryProperties simpleDiscoveryProperties;
+
+ public SimpleReactiveDiscoveryClient(
+ SimpleReactiveDiscoveryProperties simpleDiscoveryProperties) {
+ this.simpleDiscoveryProperties = simpleDiscoveryProperties;
+ }
+
+ @Override
+ public String description() {
+ return "Simple Reactive Discovery Client";
+ }
+
+ @Override
+ public Flux getInstances(String serviceId) {
+ return this.simpleDiscoveryProperties.getInstances(serviceId);
+ }
+
+ @Override
+ public Flux getServices() {
+ return Flux.fromIterable(this.simpleDiscoveryProperties.getInstances().keySet());
+ }
+
+ @Override
+ public int getOrder() {
+ return this.simpleDiscoveryProperties.getOrder();
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfiguration.java
new file mode 100644
index 00000000..f5681a15
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfiguration.java
@@ -0,0 +1,109 @@
+/*
+ * 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.client.discovery.simple.reactive;
+
+import java.net.URI;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.context.WebServerInitializedEvent;
+import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
+import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled;
+import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration;
+import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties;
+import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator;
+import org.springframework.cloud.commons.util.InetUtils;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+
+/**
+ * Spring Boot auto-configuration for simple properties-based reactive discovery client.
+ *
+ * @author Tim Ysewyn
+ * @since 2.2.0
+ */
+@Configuration
+@ConditionalOnDiscoveryEnabled
+@ConditionalOnReactiveDiscoveryEnabled
+@EnableConfigurationProperties(DiscoveryClientHealthIndicatorProperties.class)
+@AutoConfigureBefore(ReactiveCommonsClientAutoConfiguration.class)
+public class SimpleReactiveDiscoveryClientAutoConfiguration
+ implements ApplicationListener {
+
+ @Autowired(required = false)
+ private ServerProperties server;
+
+ @Value("${spring.application.name:application}")
+ private String serviceId;
+
+ @Autowired
+ private InetUtils inet;
+
+ private int port = 0;
+
+ private SimpleReactiveDiscoveryProperties simple = new SimpleReactiveDiscoveryProperties();
+
+ @Bean
+ public SimpleReactiveDiscoveryProperties simpleReactiveDiscoveryProperties() {
+ simple.getLocal().setServiceId(serviceId);
+ simple.getLocal().setUri(URI.create("http://"
+ + inet.findFirstNonLoopbackHostInfo().getHostname() + ":" + findPort()));
+ return simple;
+ }
+
+ @Bean
+ @Order
+ public SimpleReactiveDiscoveryClient simpleReactiveDiscoveryClient() {
+ return new SimpleReactiveDiscoveryClient(simpleReactiveDiscoveryProperties());
+ }
+
+ @Bean
+ @ConditionalOnProperty(
+ value = "spring.cloud.discovery.client.health-indicator.enabled",
+ matchIfMissing = true)
+ public ReactiveDiscoveryClientHealthIndicator simpleReactiveDiscoveryClientHealthIndicator(
+ DiscoveryClientHealthIndicatorProperties properties) {
+ return new ReactiveDiscoveryClientHealthIndicator(simpleReactiveDiscoveryClient(),
+ properties);
+ }
+
+ private int findPort() {
+ if (port > 0) {
+ return port;
+ }
+ if (server != null && server.getPort() != null && server.getPort() > 0) {
+ return server.getPort();
+ }
+ return 8080;
+ }
+
+ @Override
+ public void onApplicationEvent(WebServerInitializedEvent webServerInitializedEvent) {
+ port = webServerInitializedEvent.getWebServer().getPort();
+ if (port > 0) {
+ simple.getLocal().setUri(URI.create("http://"
+ + inet.findFirstNonLoopbackHostInfo().getHostname() + ":" + port));
+ }
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryProperties.java
new file mode 100644
index 00000000..11f28b4e
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryProperties.java
@@ -0,0 +1,188 @@
+/*
+ * 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.client.discovery.simple.reactive;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.PostConstruct;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+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
+ * establish the precedence of this client in the list of clients used by
+ * {@link org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient}.
+ *
+ * @author Tim Ysewyn
+ * @since 2.2.0
+ */
+@ConfigurationProperties(prefix = "spring.cloud.discovery.client.simple")
+public class SimpleReactiveDiscoveryProperties {
+
+ private Map> instances = new HashMap<>();
+
+ /**
+ * The properties of the local instance (if it exists). Users should set these
+ * properties explicitly if they are exporting data (e.g. metrics) that need to be
+ * identified by the service instance.
+ */
+ private SimpleServiceInstance local = new SimpleServiceInstance();
+
+ private int order = DiscoveryClient.DEFAULT_ORDER;
+
+ public Flux getInstances(String service) {
+ return Flux.fromIterable(instances.getOrDefault(service, emptyList()));
+ }
+
+ Map> getInstances() {
+ return instances;
+ }
+
+ public void setInstances(Map> instances) {
+ this.instances = instances;
+ }
+
+ public SimpleServiceInstance getLocal() {
+ return this.local;
+ }
+
+ public int getOrder() {
+ return this.order;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ @PostConstruct
+ public void init() {
+ for (String key : this.instances.keySet()) {
+ for (SimpleServiceInstance instance : this.instances.get(key)) {
+ instance.setServiceId(key);
+ }
+ }
+ }
+
+ /**
+ * Basic implementation of {@link ServiceInstance}.
+ */
+ public static class SimpleServiceInstance implements ServiceInstance {
+
+ /**
+ * The URI of the service instance. Will be parsed to extract the scheme, host,
+ * and port.
+ */
+ private URI uri;
+
+ private String host;
+
+ private int port;
+
+ private boolean secure;
+
+ /**
+ * Metadata for the service instance. Can be used by discovery clients to modify
+ * their behaviour per instance, e.g. when load balancing.
+ */
+ private Map metadata = new LinkedHashMap<>();
+
+ /**
+ * The unique identifier or name for the service instance.
+ */
+ private String instanceId;
+
+ /**
+ * The identifier or name for the service. Multiple instances might share the same
+ * service ID.
+ */
+ private String serviceId;
+
+ public SimpleServiceInstance() {
+ }
+
+ public SimpleServiceInstance(URI uri) {
+ setUri(uri);
+ }
+
+ @Override
+ public String getInstanceId() {
+ return this.instanceId;
+ }
+
+ public void setInstanceId(String id) {
+ this.instanceId = id;
+ }
+
+ @Override
+ public String getServiceId() {
+ return this.serviceId;
+ }
+
+ public void setServiceId(String id) {
+ this.serviceId = id;
+ }
+
+ @Override
+ public String getHost() {
+ return this.host;
+ }
+
+ @Override
+ public int getPort() {
+ return this.port;
+ }
+
+ @Override
+ public boolean isSecure() {
+ return this.secure;
+ }
+
+ @Override
+ public URI getUri() {
+ return this.uri;
+ }
+
+ public void setUri(URI uri) {
+ this.uri = uri;
+ this.host = this.uri.getHost();
+ this.port = this.uri.getPort();
+ String scheme = this.uri.getScheme();
+ if ("https".equals(scheme)) {
+ this.secure = true;
+ }
+ }
+
+ @Override
+ public Map getMetadata() {
+ return this.metadata;
+ }
+
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/resources/META-INF/spring.factories b/spring-cloud-commons/src/main/resources/META-INF/spring.factories
index 16d4bb38..bd9662cb 100644
--- a/spring-cloud-commons/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-commons/src/main/resources/META-INF/spring.factories
@@ -1,9 +1,12 @@
# AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
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.noop.NoopDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration,\
+org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration,\
org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration,\
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/CommonsClientAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/CommonsClientAutoConfigurationTests.java
index ef0c5bd9..0da31750 100644
--- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/CommonsClientAutoConfigurationTests.java
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/CommonsClientAutoConfigurationTests.java
@@ -19,18 +19,19 @@ package org.springframework.cloud.client;
import org.junit.Test;
import org.springframework.beans.BeansException;
-import org.springframework.boot.WebApplicationType;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.client.actuator.FeaturesEndpoint;
import org.springframework.cloud.client.actuator.HasFeatures;
import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicator;
import org.springframework.cloud.client.discovery.health.DiscoveryCompositeHealthIndicator;
-import org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration;
+import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration;
+import org.springframework.cloud.commons.util.UtilAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
@@ -39,66 +40,95 @@ import static org.assertj.core.api.BDDAssertions.then;
/**
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
+ * @author Tim Ysewyn
*/
public class CommonsClientAutoConfigurationTests {
+ ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
+ .withConfiguration(
+ AutoConfigurations.of(HealthEndpointAutoConfiguration.class,
+ CommonsClientAutoConfiguration.class,
+ SimpleDiscoveryClientAutoConfiguration.class,
+ UtilAutoConfiguration.class));
+
@Test
public void beansCreatedNormally() {
- try (ConfigurableApplicationContext ctxt = init()) {
+ applicationContextRunner.run(ctxt -> {
then(ctxt.getBean(DiscoveryClientHealthIndicator.class)).isNotNull();
then(ctxt.getBean(DiscoveryCompositeHealthIndicator.class)).isNotNull();
then(ctxt.getBean(FeaturesEndpoint.class)).isNotNull();
then(ctxt.getBeansOfType(HasFeatures.class).values()).isNotEmpty();
- }
+ });
}
@Test
public void disableAll() {
- try (ConfigurableApplicationContext ctxt = init(
- "spring.cloud.discovery.enabled=false")) {
- assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class);
- assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class);
- then(ctxt.getBean(FeaturesEndpoint.class)).isNotNull(); // features
- // actuator
- // is
- // independent
- // of
- // discovery
- assertBeanNonExistant(ctxt, HasFeatures.class);
- }
+ applicationContextRunner
+ .withPropertyValues("spring.cloud.discovery.enabled=false").run(ctxt -> {
+ assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class);
+ assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class);
+ then(ctxt.getBean(FeaturesEndpoint.class)).isNotNull();
+ // features actuator is independent of discovery
+ assertBeanNonExistant(ctxt, HasFeatures.class);
+ });
+ }
+
+ @Test
+ public void disableBlocking() {
+ applicationContextRunner
+ .withPropertyValues("spring.cloud.discovery.blocking.enabled=false")
+ .run(ctxt -> {
+ assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class);
+ assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class);
+ then(ctxt.getBean(FeaturesEndpoint.class)).isNotNull();
+ // features actuator is independent of discovery
+ assertBeanNonExistant(ctxt, HasFeatures.class);
+ });
}
@Test
public void disableAllIndividually() {
- try (ConfigurableApplicationContext ctxt = init(
+ applicationContextRunner.withPropertyValues(
"spring.cloud.discovery.client.health-indicator.enabled=false",
"spring.cloud.discovery.client.composite-indicator.enabled=false",
- "spring.cloud.features.enabled=false")) {
- assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class);
- assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class);
- assertBeanNonExistant(ctxt, FeaturesEndpoint.class);
- }
+ "spring.cloud.features.enabled=false").run(ctxt -> {
+ assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class);
+ assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class);
+ assertBeanNonExistant(ctxt, FeaturesEndpoint.class);
+ });
}
@Test
public void disableHealthIndicator() {
- try (ConfigurableApplicationContext ctxt = init(
- "spring.cloud.discovery.client.health-indicator.enabled=false")) {
- assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class);
- assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class);
- }
+ applicationContextRunner
+ .withPropertyValues(
+ "spring.cloud.discovery.client.health-indicator.enabled=false")
+ .run(ctxt -> {
+ assertBeanNonExistant(ctxt, DiscoveryClientHealthIndicator.class);
+ assertBeanNonExistant(ctxt, DiscoveryCompositeHealthIndicator.class);
+ });
}
@Test
public void conditionalOnDiscoveryEnabledWorks() {
- try (ConfigurableApplicationContext context = init(
- "spring.cloud.discovery.enabled=false")) {
- assertBeanNonExistant(context, TestBean.class);
- }
- try (ConfigurableApplicationContext context = init(
- "spring.cloud.discovery.enabled=true")) {
- assertThat(context.getBean(TestBean.class)).isNotNull();
- }
+ applicationContextRunner.withUserConfiguration(DiscoveryEnabledConfig.class)
+ .withPropertyValues("spring.cloud.discovery.enabled=false")
+ .run(context -> assertBeanNonExistant(context, TestBean.class));
+ applicationContextRunner.withUserConfiguration(DiscoveryEnabledConfig.class)
+ .withPropertyValues("spring.cloud.discovery.enabled=true")
+ .run(context -> assertThat(context.getBean(TestBean.class)).isNotNull());
+ }
+
+ @Test
+ public void conditionalOnBlockingDiscoveryEnabledWorks() {
+ applicationContextRunner
+ .withUserConfiguration(BlockingDiscoveryEnabledConfig.class)
+ .withPropertyValues("spring.cloud.discovery.blocking.enabled=false")
+ .run(context -> assertBeanNonExistant(context, TestBean.class));
+ applicationContextRunner
+ .withUserConfiguration(BlockingDiscoveryEnabledConfig.class)
+ .withPropertyValues("spring.cloud.discovery.blocking.enabled=true")
+ .run(context -> assertThat(context.getBean(TestBean.class)).isNotNull());
}
private void assertBeanNonExistant(ConfigurableApplicationContext ctxt,
@@ -112,18 +142,6 @@ public class CommonsClientAutoConfigurationTests {
}
}
- protected ConfigurableApplicationContext init(String... pairs) {
- return new SpringApplicationBuilder().web(WebApplicationType.NONE)
- .sources(Config.class).properties(pairs).run();
- }
-
- @Configuration
- @EnableAutoConfiguration
- @Import({ NoopDiscoveryClientAutoConfiguration.class, DiscoveryEnabledConfig.class })
- protected static class Config {
-
- }
-
@Configuration
@ConditionalOnDiscoveryEnabled
protected static class DiscoveryEnabledConfig {
@@ -135,6 +153,17 @@ public class CommonsClientAutoConfigurationTests {
}
+ @TestConfiguration
+ @ConditionalOnBlockingDiscoveryEnabled
+ protected static class BlockingDiscoveryEnabledConfig {
+
+ @Bean
+ TestBean testBean() {
+ return new TestBean();
+ }
+
+ }
+
private static class TestBean {
}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfigurationTests.java
new file mode 100644
index 00000000..3ededeff
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/ReactiveCommonsClientAutoConfigurationTests.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2019-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.client;
+
+import org.junit.Test;
+
+import org.springframework.beans.BeansException;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.cloud.client.actuator.FeaturesEndpoint;
+import org.springframework.cloud.client.actuator.HasFeatures;
+import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator;
+import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryCompositeHealthContributor;
+import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration;
+import org.springframework.cloud.commons.util.UtilAutoConfiguration;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.assertj.core.api.BDDAssertions.then;
+
+/**
+ * @author Tim Ysewyn
+ */
+public class ReactiveCommonsClientAutoConfigurationTests {
+
+ ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(CommonsClientAutoConfiguration.class,
+ SimpleReactiveDiscoveryClientAutoConfiguration.class,
+ UtilAutoConfiguration.class,
+ ReactiveCommonsClientAutoConfiguration.class));
+
+ @Test
+ public void beansCreatedNormally() {
+ applicationContextRunner.run(context -> {
+ then(context.getBean(ReactiveDiscoveryClientHealthIndicator.class))
+ .isNotNull();
+ then(context.getBean(ReactiveDiscoveryCompositeHealthContributor.class))
+ .isNotNull();
+ then(context.getBean(FeaturesEndpoint.class)).isNotNull();
+ then(context.getBeansOfType(HasFeatures.class).values()).isNotEmpty();
+ });
+ }
+
+ @Test
+ public void disableAll() {
+ applicationContextRunner
+ .withPropertyValues("spring.cloud.discovery.enabled=false")
+ .run(context -> {
+ assertBeanNonExistant(context,
+ ReactiveDiscoveryClientHealthIndicator.class);
+ assertBeanNonExistant(context,
+ ReactiveDiscoveryCompositeHealthContributor.class);
+ // features actuator is independent of discovery
+ then(context.getBean(FeaturesEndpoint.class)).isNotNull();
+ assertBeanNonExistant(context, HasFeatures.class);
+ });
+ }
+
+ @Test
+ public void disableReactive() {
+ applicationContextRunner
+ .withPropertyValues("spring.cloud.discovery.reactive.enabled=false")
+ .run(context -> {
+ assertBeanNonExistant(context,
+ ReactiveDiscoveryClientHealthIndicator.class);
+ assertBeanNonExistant(context,
+ ReactiveDiscoveryCompositeHealthContributor.class);
+ // features actuator is independent of discovery
+ then(context.getBean(FeaturesEndpoint.class)).isNotNull();
+ assertBeanNonExistant(context, HasFeatures.class);
+ });
+ }
+
+ @Test
+ public void disableAllIndividually() {
+ applicationContextRunner.withPropertyValues(
+ "spring.cloud.discovery.client.health-indicator.enabled=false",
+ "spring.cloud.discovery.client.composite-indicator.enabled=false",
+ "spring.cloud.features.enabled=false").run(context -> {
+ assertBeanNonExistant(context,
+ ReactiveDiscoveryClientHealthIndicator.class);
+ assertBeanNonExistant(context,
+ ReactiveDiscoveryCompositeHealthContributor.class);
+ assertBeanNonExistant(context, FeaturesEndpoint.class);
+ });
+ }
+
+ @Test
+ public void disableHealthIndicator() {
+ applicationContextRunner
+ .withPropertyValues(
+ "spring.cloud.discovery.client.health-indicator.enabled=false")
+ .run(context -> {
+ assertBeanNonExistant(context,
+ ReactiveDiscoveryClientHealthIndicator.class);
+ assertBeanNonExistant(context,
+ ReactiveDiscoveryCompositeHealthContributor.class);
+ });
+ }
+
+ @Test
+ public void conditionalOnReactiveDiscoveryEnabledWorks() {
+ ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withUserConfiguration(ReactiveDiscoveryEnabledConfig.class);
+ contextRunner.withPropertyValues("spring.cloud.discovery.reactive.enabled=false")
+ .run(context -> assertBeanNonExistant(context, TestBean.class));
+ contextRunner.withPropertyValues("spring.cloud.discovery.reactive.enabled=true")
+ .run(context -> assertThat(context.getBean(TestBean.class)).isNotNull());
+ }
+
+ private void assertBeanNonExistant(ConfigurableApplicationContext ctxt,
+ Class> beanClass) {
+ try {
+ ctxt.getBean(beanClass);
+ fail("Bean of type " + beanClass + " should not have been created");
+ }
+ catch (BeansException e) {
+ // should fail with exception
+ }
+ }
+
+ @Configuration
+ @ConditionalOnDiscoveryEnabled
+ protected static class DiscoveryEnabledConfig {
+
+ @Bean
+ TestBean testBean() {
+ return new TestBean();
+ }
+
+ }
+
+ @TestConfiguration
+ @ConditionalOnReactiveDiscoveryEnabled
+ protected static class ReactiveDiscoveryEnabledConfig {
+
+ @Bean
+ TestBean testBean() {
+ return new TestBean();
+ }
+
+ }
+
+ private static class TestBean {
+
+ }
+
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfigurationTests.java
new file mode 100644
index 00000000..a78ec7bf
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientAutoConfigurationTests.java
@@ -0,0 +1,89 @@
+/*
+ * 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.client.discovery.composite.reactive;
+
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.context.annotation.Bean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Tim Ysewyn
+ */
+class ReactiveCompositeDiscoveryClientAutoConfigurationTests {
+
+ private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations
+ .of(ReactiveCompositeDiscoveryClientAutoConfiguration.class));
+
+ @Test
+ public void shouldCreateCompositeReactiveDiscoveryClientWithoutDelegates() {
+ this.contextRunner.run((context) -> {
+ ReactiveDiscoveryClient client = context
+ .getBean(ReactiveDiscoveryClient.class);
+ assertThat(client).isNotNull();
+ assertThat(client).isInstanceOf(ReactiveCompositeDiscoveryClient.class);
+ assertThat(((ReactiveCompositeDiscoveryClient) client).getDiscoveryClients())
+ .isEmpty();
+ });
+ }
+
+ @Test
+ public void shouldCreateCompositeReactiveDiscoveryClientWithDelegate() {
+ this.contextRunner.withUserConfiguration(Configuration.class).run((context) -> {
+ ReactiveDiscoveryClient client = context
+ .getBean(ReactiveDiscoveryClient.class);
+ assertThat(client).isNotNull();
+ assertThat(client).isInstanceOf(ReactiveCompositeDiscoveryClient.class);
+ assertThat(((ReactiveCompositeDiscoveryClient) client).getDiscoveryClients())
+ .hasSize(1);
+ });
+ }
+
+ @TestConfiguration
+ static class Configuration {
+
+ @Bean
+ ReactiveDiscoveryClient discoveryClient() {
+ return new ReactiveDiscoveryClient() {
+ @Override
+ public String description() {
+ return "Reactive Test Discovery Client";
+ }
+
+ @Override
+ public Flux getInstances(String serviceId) {
+ return Flux.empty();
+ }
+
+ @Override
+ public Flux getServices() {
+ return Flux.empty();
+ }
+ };
+ }
+
+ }
+
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientTests.java
new file mode 100644
index 00000000..b64d805a
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/composite/reactive/ReactiveCompositeDiscoveryClientTests.java
@@ -0,0 +1,124 @@
+/*
+ * 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.client.discovery.composite.reactive;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import reactor.core.publisher.Flux;
+import reactor.test.StepVerifier;
+import reactor.test.publisher.TestPublisher;
+
+import org.springframework.cloud.client.DefaultServiceInstance;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Tim Ysewyn
+ */
+@ExtendWith(MockitoExtension.class)
+class ReactiveCompositeDiscoveryClientTests {
+
+ @Mock
+ private ReactiveDiscoveryClient discoveryClient1;
+
+ @Mock
+ private ReactiveDiscoveryClient discoveryClient2;
+
+ @Test
+ public void shouldReturnEmptyFluxOfServices() {
+ ReactiveCompositeDiscoveryClient client = new ReactiveCompositeDiscoveryClient(
+ emptyList());
+
+ Flux services = client.getServices();
+
+ StepVerifier.create(services).expectComplete().verify();
+ }
+
+ @Test
+ public void shouldReturnFluxOfServices() {
+ TestPublisher discoveryClient1Publisher = TestPublisher.createCold();
+ discoveryClient1Publisher.emit("serviceAFromClient1");
+ discoveryClient1Publisher.emit("serviceBFromClient1");
+ discoveryClient1Publisher.complete();
+
+ TestPublisher discoveryClient2Publisher = TestPublisher.createCold();
+ discoveryClient2Publisher.emit("serviceCFromClient2");
+ discoveryClient2Publisher.complete();
+
+ when(discoveryClient1.getServices()).thenReturn(discoveryClient1Publisher.flux());
+ when(discoveryClient2.getServices()).thenReturn(discoveryClient2Publisher.flux());
+
+ ReactiveCompositeDiscoveryClient client = new ReactiveCompositeDiscoveryClient(
+ asList(discoveryClient1, discoveryClient2));
+
+ assertThat(client.description()).isEqualTo("Composite Reactive Discovery Client");
+
+ Flux services = client.getServices();
+
+ StepVerifier.create(services).expectNext("serviceAFromClient1")
+ .expectNext("serviceBFromClient1").expectNext("serviceCFromClient2")
+ .expectComplete().verify();
+ }
+
+ @Test
+ public void shouldReturnEmptyFluxOfServiceInstances() {
+ ReactiveCompositeDiscoveryClient client = new ReactiveCompositeDiscoveryClient(
+ emptyList());
+
+ Flux instances = client.getInstances("service");
+
+ StepVerifier.create(instances).expectComplete().verify();
+ }
+
+ @Test
+ public void shouldReturnFluxOfServiceInstances() {
+ DefaultServiceInstance serviceInstance1 = new DefaultServiceInstance("instance",
+ "service", "localhost", 8080, false);
+ DefaultServiceInstance serviceInstance2 = new DefaultServiceInstance("instance2",
+ "service", "localhost", 8080, false);
+ TestPublisher discoveryClient1Publisher = TestPublisher
+ .createCold();
+ discoveryClient1Publisher.emit(serviceInstance1);
+ discoveryClient1Publisher.emit(serviceInstance2);
+ discoveryClient1Publisher.complete();
+
+ TestPublisher discoveryClient2Publisher = TestPublisher
+ .createCold();
+ discoveryClient2Publisher.complete();
+
+ when(discoveryClient1.getInstances("service"))
+ .thenReturn(discoveryClient1Publisher.flux());
+ when(discoveryClient2.getInstances("service"))
+ .thenReturn(discoveryClient2Publisher.flux());
+
+ ReactiveCompositeDiscoveryClient client = new ReactiveCompositeDiscoveryClient(
+ asList(discoveryClient1, discoveryClient2));
+
+ Flux instances = client.getInstances("service");
+
+ StepVerifier.create(instances).expectNext(serviceInstance1)
+ .expectNext(serviceInstance2).expectComplete().verify();
+ }
+
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicatorTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicatorTests.java
new file mode 100644
index 00000000..bf7091e5
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryClientHealthIndicatorTests.java
@@ -0,0 +1,115 @@
+/*
+ * 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.client.discovery.health.reactive;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.Status;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent;
+import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties;
+import org.springframework.core.Ordered;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Tim Ysewyn
+ */
+@ExtendWith(MockitoExtension.class)
+class ReactiveDiscoveryClientHealthIndicatorTests {
+
+ @Mock
+ private ReactiveDiscoveryClient discoveryClient;
+
+ @Mock
+ private DiscoveryClientHealthIndicatorProperties properties;
+
+ @InjectMocks
+ private ReactiveDiscoveryClientHealthIndicator indicator;
+
+ @Test
+ public void shouldReturnCorrectOrder() {
+ assertThat(indicator.getOrder()).isEqualTo(Ordered.HIGHEST_PRECEDENCE);
+ indicator.setOrder(0);
+ assertThat(indicator.getOrder()).isEqualTo(0);
+ }
+
+ @Test
+ public void shouldReturnUnknownStatusWhenNotInitialized() {
+ Health expectedHealth = Health.status(
+ new Status(Status.UNKNOWN.getCode(), "Discovery Client not initialized"))
+ .build();
+ Mono health = indicator.health();
+ StepVerifier.create(health).expectNext(expectedHealth).expectComplete().verify();
+ }
+
+ @Test
+ public void shouldReturnUpStatusWithoutServices() {
+ when(discoveryClient.description()).thenReturn("Mocked Service Discovery Client");
+ when(discoveryClient.getServices()).thenReturn(Flux.empty());
+ Health expectedHealth = Health.status(new Status(Status.UP.getCode(), ""))
+ .withDetail("services", emptyList()).build();
+
+ indicator.onApplicationEvent(new InstanceRegisteredEvent<>(this, null));
+ Mono health = indicator.health();
+
+ assertThat(indicator.getName()).isEqualTo("Mocked Service Discovery Client");
+ StepVerifier.create(health).expectNext(expectedHealth).expectComplete().verify();
+ }
+
+ @Test
+ public void shouldReturnUpStatusWithServices() {
+ when(discoveryClient.getServices()).thenReturn(Flux.just("service"));
+ when(properties.isIncludeDescription()).thenReturn(true);
+ when(discoveryClient.description()).thenReturn("Mocked Service Discovery Client");
+ Health expectedHealth = Health
+ .status(new Status(Status.UP.getCode(),
+ "Mocked Service Discovery Client"))
+ .withDetail("services", singletonList("service")).build();
+
+ indicator.onApplicationEvent(new InstanceRegisteredEvent<>(this, null));
+ Mono health = indicator.health();
+
+ assertThat(indicator.getName()).isEqualTo("Mocked Service Discovery Client");
+ StepVerifier.create(health).expectNext(expectedHealth).expectComplete().verify();
+ }
+
+ @Test
+ public void shouldReturnDownStatusWhenServicesCouldNotBeRetrieved() {
+ RuntimeException ex = new RuntimeException("something went wrong");
+ Health expectedHealth = Health.down(ex).build();
+ when(discoveryClient.getServices()).thenReturn(Flux.error(ex));
+
+ indicator.onApplicationEvent(new InstanceRegisteredEvent<>(this, null));
+ Mono health = indicator.health();
+
+ StepVerifier.create(health).expectNext(expectedHealth).expectComplete()
+ .verifyThenAssertThat();
+ }
+
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributorTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributorTests.java
new file mode 100644
index 00000000..6e321106
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/health/reactive/ReactiveDiscoveryCompositeHealthContributorTests.java
@@ -0,0 +1,82 @@
+/*
+ * 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.client.discovery.health.reactive;
+
+import java.util.Iterator;
+
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.NamedContributor;
+import org.springframework.boot.actuate.health.ReactiveHealthContributor;
+import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Tim Ysewyn
+ */
+class ReactiveDiscoveryCompositeHealthContributorTests {
+
+ @Test
+ void shouldReturnEmptyIterator() {
+ ReactiveDiscoveryCompositeHealthContributor healthContributor = new ReactiveDiscoveryCompositeHealthContributor(
+ emptyList());
+ assertThat(healthContributor.iterator().hasNext()).isFalse();
+ }
+
+ @Test
+ void shouldReturnNullForUnknownContributor() {
+ ReactiveDiscoveryCompositeHealthContributor healthContributor = new ReactiveDiscoveryCompositeHealthContributor(
+ emptyList());
+ assertThat(healthContributor.getContributor("unknown")).isNull();
+ }
+
+ @Test
+ void shouldReturnKnownContributor() {
+ ReactiveDiscoveryHealthIndicator indicator = mock(
+ ReactiveDiscoveryHealthIndicator.class);
+ Health health = Health.up().build();
+ when(indicator.getName()).thenReturn("known");
+ when(indicator.health()).thenReturn(Mono.just(health));
+
+ ReactiveDiscoveryCompositeHealthContributor healthContributor = new ReactiveDiscoveryCompositeHealthContributor(
+ singletonList(indicator));
+
+ assertThat(healthContributor.getContributor("known")).isNotNull();
+ Iterator> iterator = healthContributor
+ .iterator();
+ assertThat(iterator.hasNext()).isTrue();
+ NamedContributor contributor = iterator.next();
+ assertThat(contributor).isNotNull();
+ assertThat(contributor.getName()).isEqualTo("known");
+ assertThat(contributor.getContributor()).isNotNull();
+ assertThat(contributor.getContributor())
+ .isInstanceOf(ReactiveHealthIndicator.class);
+ ReactiveHealthIndicator healthIndicator = (ReactiveHealthIndicator) contributor
+ .getContributor();
+ StepVerifier.create(healthIndicator.getHealth(true)).expectNext(health)
+ .expectComplete().verify();
+ }
+
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfigurationTests.java
new file mode 100644
index 00000000..47b56b5d
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientAutoConfigurationTests.java
@@ -0,0 +1,92 @@
+/*
+ * 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.client.discovery.simple.reactive;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.commons.util.InetUtils;
+import org.springframework.cloud.commons.util.UtilAutoConfiguration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Tim Ysewyn
+ */
+class SimpleReactiveDiscoveryClientAutoConfigurationTests {
+
+ private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(
+ SimpleReactiveDiscoveryClientAutoConfiguration.class,
+ UtilAutoConfiguration.class));
+
+ @Test
+ public void shouldUseDefaults() {
+ this.contextRunner.run((context) -> {
+ ReactiveDiscoveryClient client = context
+ .getBean(ReactiveDiscoveryClient.class);
+ assertThat(client).isNotNull();
+ assertThat(client.getOrder())
+ .isEqualTo(ReactiveDiscoveryClient.DEFAULT_ORDER);
+ InetUtils inet = context.getBean(InetUtils.class);
+ assertThat(inet).isNotNull();
+ SimpleReactiveDiscoveryProperties properties = context
+ .getBean(SimpleReactiveDiscoveryProperties.class);
+ assertThat(properties).isNotNull();
+ assertThat(properties.getLocal().getServiceId()).isEqualTo("application");
+ assertThat(properties.getLocal().getHost())
+ .isEqualTo(inet.findFirstNonLoopbackHostInfo().getHostname());
+ assertThat(properties.getLocal().getPort()).isEqualTo(8080);
+ });
+ }
+
+ @Test
+ public void shouldUseCustomConfiguration() {
+ this.contextRunner.withUserConfiguration(Configuration.class)
+ .withPropertyValues("spring.application.name=my-service",
+ "spring.cloud.discovery.client.simple.order=1",
+ "server.port=8443")
+ .run((context) -> {
+ ReactiveDiscoveryClient client = context
+ .getBean(ReactiveDiscoveryClient.class);
+ assertThat(client).isNotNull();
+ assertThat(client.getOrder()).isEqualTo(1);
+ InetUtils inet = context.getBean(InetUtils.class);
+ assertThat(inet).isNotNull();
+ SimpleReactiveDiscoveryProperties properties = context
+ .getBean(SimpleReactiveDiscoveryProperties.class);
+ assertThat(properties).isNotNull();
+ assertThat(properties.getLocal().getServiceId())
+ .isEqualTo("my-service");
+ assertThat(properties.getLocal().getHost())
+ .isEqualTo(inet.findFirstNonLoopbackHostInfo().getHostname());
+ assertThat(properties.getLocal().getPort()).isEqualTo(8443);
+ });
+ }
+
+ @TestConfiguration
+ @EnableConfigurationProperties(ServerProperties.class)
+ static class Configuration {
+
+ }
+
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientTests.java
new file mode 100644
index 00000000..1c9c5cb5
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/discovery/simple/reactive/SimpleReactiveDiscoveryClientTests.java
@@ -0,0 +1,82 @@
+/*
+ * 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.client.discovery.simple.reactive;
+
+import java.net.URI;
+import java.util.Arrays;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.test.StepVerifier;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryProperties.SimpleServiceInstance;
+
+import static java.util.Collections.singletonMap;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Tim Ysewyn
+ */
+public class SimpleReactiveDiscoveryClientTests {
+
+ private final SimpleServiceInstance service1Inst1 = new SimpleServiceInstance(
+ URI.create("http://host1:8080"));
+
+ private final SimpleServiceInstance service1Inst2 = new SimpleServiceInstance(
+ URI.create("https://host2:8443"));
+
+ private SimpleReactiveDiscoveryClient client;
+
+ @BeforeEach
+ public void setUp() {
+ SimpleReactiveDiscoveryProperties simpleReactiveDiscoveryProperties = new SimpleReactiveDiscoveryProperties();
+ simpleReactiveDiscoveryProperties.setInstances(
+ singletonMap("service", Arrays.asList(service1Inst1, service1Inst2)));
+ simpleReactiveDiscoveryProperties.init();
+ this.client = new SimpleReactiveDiscoveryClient(
+ simpleReactiveDiscoveryProperties);
+ }
+
+ @Test
+ public void verifyDefaults() {
+ assertThat(client.description()).isEqualTo("Simple Reactive Discovery Client");
+ assertThat(client.getOrder()).isEqualTo(ReactiveDiscoveryClient.DEFAULT_ORDER);
+ }
+
+ @Test
+ public void shouldReturnFluxOfServices() {
+ Flux services = this.client.getServices();
+ StepVerifier.create(services).expectNext("service").expectComplete().verify();
+ }
+
+ @Test
+ public void shouldReturnEmptyFluxForNonExistingService() {
+ Flux instances = this.client.getInstances("undefined");
+ StepVerifier.create(instances).expectComplete();
+ }
+
+ @Test
+ public void shouldReturnFluxOfServiceInstances() {
+ Flux services = this.client.getInstances("service");
+ StepVerifier.create(services).expectNext(service1Inst1).expectNext(service1Inst2)
+ .expectComplete().verify();
+ }
+
+}