Added support for reactive service discovery client

This commit is contained in:
Tim Ysewyn
2019-09-11 17:39:10 +02:00
committed by GitHub
parent 19bab039a9
commit d6be04468c
25 changed files with 1792 additions and 58 deletions

View File

@@ -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`.

View File

@@ -163,5 +163,10 @@
<artifactId>spring-cloud-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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

View File

@@ -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
* <code>spring.cloud.discovery.blocking.enabled</code>.
*
* @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 {
}

View File

@@ -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
* <code>spring.cloud.discovery.reactive.enabled</code>. 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 {
}

View File

@@ -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<ReactiveDiscoveryHealthIndicator> indicators) {
return new ReactiveDiscoveryCompositeHealthContributor(indicators);
}
@Bean
public HasFeatures reactiveCommonsFeatures() {
return HasFeatures.abstractFeatures(ReactiveDiscoveryClient.class,
ReactiveLoadBalancer.class);
}
}
}

View File

@@ -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<ServiceInstance> getInstances(String serviceId);
/**
* @return All known service IDs.
*/
Flux<String> getServices();
/**
* Default implementation for getting order of discovery clients.
* @return order
*/
@Override
default int getOrder() {
return DEFAULT_ORDER;
}
}

View File

@@ -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<ReactiveDiscoveryClient> discoveryClients;
public ReactiveCompositeDiscoveryClient(
List<ReactiveDiscoveryClient> discoveryClients) {
AnnotationAwareOrderComparator.sort(discoveryClients);
this.discoveryClients = discoveryClients;
}
@Override
public String description() {
return "Composite Reactive Discovery Client";
}
@Override
public Flux<ServiceInstance> getInstances(String serviceId) {
if (discoveryClients == null || discoveryClients.isEmpty()) {
return Flux.empty();
}
List<Flux<ServiceInstance>> serviceInstances = new ArrayList<>();
for (ReactiveDiscoveryClient discoveryClient : discoveryClients) {
serviceInstances.add(discoveryClient.getInstances(serviceId));
}
return Flux.first(serviceInstances);
}
@Override
public Flux<String> getServices() {
if (discoveryClients == null || discoveryClients.isEmpty()) {
return Flux.empty();
}
return Flux.fromIterable(discoveryClients)
.flatMap(ReactiveDiscoveryClient::getServices);
}
List<ReactiveDiscoveryClient> getDiscoveryClients() {
return discoveryClients;
}
}

View File

@@ -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<ReactiveDiscoveryClient> discoveryClients) {
return new ReactiveCompositeDiscoveryClient(discoveryClients);
}
}

View File

@@ -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<InstanceRegisteredEvent<?>> {
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> 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<Health> 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;
}
}

View File

@@ -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<String, ReactiveDiscoveryHealthIndicator> indicators;
public ReactiveDiscoveryCompositeHealthContributor(
Collection<ReactiveDiscoveryHealthIndicator> 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<NamedContributor<ReactiveHealthContributor>> iterator() {
return indicators.values().stream().map(this::asNamedContributor).iterator();
}
private NamedContributor<ReactiveHealthContributor> asNamedContributor(
ReactiveDiscoveryHealthIndicator indicator) {
return new NamedContributor<ReactiveHealthContributor>() {
@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;
}
}

View File

@@ -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> health();
}

View File

@@ -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<WebServerInitializedEvent> {
@Autowired(required = false)
private ServerProperties server;
@Autowired
private ApplicationContext context;
@Value("${spring.application.name:application}")
private String serviceId;

View File

@@ -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<ServiceInstance> getInstances(String serviceId) {
return this.simpleDiscoveryProperties.getInstances(serviceId);
}
@Override
public Flux<String> getServices() {
return Flux.fromIterable(this.simpleDiscoveryProperties.getInstances().keySet());
}
@Override
public int getOrder() {
return this.simpleDiscoveryProperties.getOrder();
}
}

View File

@@ -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<WebServerInitializedEvent> {
@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));
}
}
}

View File

@@ -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<String, List<SimpleServiceInstance>> 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<ServiceInstance> getInstances(String service) {
return Flux.fromIterable(instances.getOrDefault(service, emptyList()));
}
Map<String, List<SimpleServiceInstance>> getInstances() {
return instances;
}
public void setInstances(Map<String, List<SimpleServiceInstance>> 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<String, String> 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<String, String> getMetadata() {
return this.metadata;
}
}
}

View File

@@ -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,\

View File

@@ -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 {
}

View File

@@ -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 {
}
}

View File

@@ -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<ServiceInstance> getInstances(String serviceId) {
return Flux.empty();
}
@Override
public Flux<String> getServices() {
return Flux.empty();
}
};
}
}
}

View File

@@ -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<String> services = client.getServices();
StepVerifier.create(services).expectComplete().verify();
}
@Test
public void shouldReturnFluxOfServices() {
TestPublisher<String> discoveryClient1Publisher = TestPublisher.createCold();
discoveryClient1Publisher.emit("serviceAFromClient1");
discoveryClient1Publisher.emit("serviceBFromClient1");
discoveryClient1Publisher.complete();
TestPublisher<String> 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<String> services = client.getServices();
StepVerifier.create(services).expectNext("serviceAFromClient1")
.expectNext("serviceBFromClient1").expectNext("serviceCFromClient2")
.expectComplete().verify();
}
@Test
public void shouldReturnEmptyFluxOfServiceInstances() {
ReactiveCompositeDiscoveryClient client = new ReactiveCompositeDiscoveryClient(
emptyList());
Flux<ServiceInstance> 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<ServiceInstance> discoveryClient1Publisher = TestPublisher
.createCold();
discoveryClient1Publisher.emit(serviceInstance1);
discoveryClient1Publisher.emit(serviceInstance2);
discoveryClient1Publisher.complete();
TestPublisher<ServiceInstance> 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<ServiceInstance> instances = client.getInstances("service");
StepVerifier.create(instances).expectNext(serviceInstance1)
.expectNext(serviceInstance2).expectComplete().verify();
}
}

View File

@@ -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> 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> 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> 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> health = indicator.health();
StepVerifier.create(health).expectNext(expectedHealth).expectComplete()
.verifyThenAssertThat();
}
}

View File

@@ -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<NamedContributor<ReactiveHealthContributor>> iterator = healthContributor
.iterator();
assertThat(iterator.hasNext()).isTrue();
NamedContributor<ReactiveHealthContributor> 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();
}
}

View File

@@ -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 {
}
}

View File

@@ -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<String> services = this.client.getServices();
StepVerifier.create(services).expectNext("service").expectComplete().verify();
}
@Test
public void shouldReturnEmptyFluxForNonExistingService() {
Flux<ServiceInstance> instances = this.client.getInstances("undefined");
StepVerifier.create(instances).expectComplete();
}
@Test
public void shouldReturnFluxOfServiceInstances() {
Flux<ServiceInstance> services = this.client.getInstances("service");
StepVerifier.create(services).expectNext(service1Inst1).expectNext(service1Inst2)
.expectComplete().verify();
}
}