diff --git a/pom.xml b/pom.xml
index 9dfbf2c7..ab3b0b2a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -126,6 +126,7 @@
spring-cloud-context
spring-cloud-context-integration-tests
spring-cloud-commons
+ spring-cloud-loadbalancer
spring-cloud-starter
docs
diff --git a/spring-cloud-commons-dependencies/pom.xml b/spring-cloud-commons-dependencies/pom.xml
index c7e8642c..74e1dd83 100644
--- a/spring-cloud-commons-dependencies/pom.xml
+++ b/spring-cloud-commons-dependencies/pom.xml
@@ -43,6 +43,11 @@
spring-cloud-context
${project.version}
+
+ org.springframework.cloud
+ spring-cloud-loadbalancer
+ ${project.version}
+
org.springframework.cloud
spring-cloud-starter
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/DefaultServiceInstance.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/DefaultServiceInstance.java
index d9498482..69a7a4b9 100644
--- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/DefaultServiceInstance.java
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/DefaultServiceInstance.java
@@ -50,7 +50,7 @@ public class DefaultServiceInstance implements ServiceInstance {
public DefaultServiceInstance(String serviceId, String host, int port,
boolean secure) {
- this(serviceId, host, port, secure, new LinkedHashMap());
+ this(serviceId, host, port, secure, new LinkedHashMap<>());
}
@Override
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/CompletionContext.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/CompletionContext.java
new file mode 100644
index 00000000..23fa0899
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/CompletionContext.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2013-2017 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.client.loadbalancer.reactive;
+
+import org.springframework.core.style.ToStringCreator;
+
+/**
+ * @author Spencer Gibb
+ */
+//TODO: add metrics
+public class CompletionContext {
+
+ public enum Status {
+ /** Request was handled successfully */
+ SUCCESSS,
+ /** Request reached the server but failed due to timeout or internal error */
+ FAILED,
+ /** Request did not go off box and should not be counted for statistics */
+ DISCARD,
+ }
+
+ private final Status status;
+ private final Throwable throwable;
+
+ public CompletionContext(Status status) {
+ this(status, null);
+ }
+
+ public CompletionContext(Status status, Throwable throwable) {
+ this.status = status;
+ this.throwable = throwable;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ @Override
+ public String toString() {
+ ToStringCreator to = new ToStringCreator(this);
+ to.append("status", status);
+ to.append("throwable", throwable);
+ return to.toString();
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultRequest.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultRequest.java
new file mode 100644
index 00000000..2ac7dc93
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultRequest.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2013-2017 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.client.loadbalancer.reactive;
+
+/**
+ * @author Spencer Gibb
+ */
+public class DefaultRequest implements Request {
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultResponse.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultResponse.java
new file mode 100644
index 00000000..340a8d9e
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/DefaultResponse.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2013-2017 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.client.loadbalancer.reactive;
+
+import org.springframework.cloud.client.ServiceInstance;
+
+/**
+ * @author Spencer Gibb
+ */
+public class DefaultResponse implements Response {
+
+ private final ServiceInstance serviceInstance;
+
+ public DefaultResponse(ServiceInstance serviceInstance) {
+ this.serviceInstance = serviceInstance;
+ }
+
+ @Override
+ public boolean hasServer() {
+ return this.serviceInstance != null;
+ }
+
+ @Override
+ public ServiceInstance getServer() {
+ return this.serviceInstance;
+ }
+
+ @Override
+ public void onComplete(CompletionContext completionContext) {
+ //TODO: implement
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/EmptyResponse.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/EmptyResponse.java
new file mode 100644
index 00000000..ce5ea1ba
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/EmptyResponse.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2013-2017 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.client.loadbalancer.reactive;
+
+import org.springframework.cloud.client.ServiceInstance;
+
+/**
+ * @author Spencer Gibb
+ */
+public class EmptyResponse implements Response {
+
+ @Override
+ public boolean hasServer() {
+ return false;
+ }
+
+ @Override
+ public ServiceInstance getServer() {
+ return null;
+ }
+
+ @Override
+ public void onComplete(CompletionContext completionContext) {
+ //TODO: implement
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java
new file mode 100644
index 00000000..c3051856
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2013-2017 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.client.loadbalancer.reactive;
+
+import org.reactivestreams.Publisher;
+
+/**
+ * @author Spencer Gibb
+ */
+public interface ReactiveLoadBalancer {
+
+ Request REQUEST = new DefaultRequest();
+
+ /**
+ * Choose the next server based on the load balancing algorithm
+ * @param request
+ * @return
+ */
+ Publisher> choose(Request request);
+
+ default Publisher> choose() { //conflicting name
+ return choose(REQUEST);
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Request.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Request.java
new file mode 100644
index 00000000..309254af
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Request.java
@@ -0,0 +1,5 @@
+package org.springframework.cloud.client.loadbalancer.reactive;
+
+public interface Request {
+ //TODO: define contents
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Response.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Response.java
new file mode 100644
index 00000000..47c14ccc
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/Response.java
@@ -0,0 +1,16 @@
+package org.springframework.cloud.client.loadbalancer.reactive;
+
+/**
+ * Response created for each request.
+ */
+public interface Response {
+ boolean hasServer();
+
+ T getServer();
+
+ /**
+ * Notification that the request completed
+ * @param completionContext
+ */
+ void onComplete(CompletionContext completionContext);
+}
diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/ClientFactoryObjectProvider.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/ClientFactoryObjectProvider.java
new file mode 100644
index 00000000..1de53ae0
--- /dev/null
+++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/ClientFactoryObjectProvider.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://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.context.named;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.lang.Nullable;
+
+import java.util.Iterator;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+/**
+ * Special ObjectProvider that allows the actual ObjectProvider to be resolved
+ * later because of the creation of the named child context.
+ * @param
+ */
+class ClientFactoryObjectProvider implements ObjectProvider {
+
+ private final NamedContextFactory clientFactory;
+ private final String name;
+ private final Class type;
+ private ObjectProvider provider;
+
+ public ClientFactoryObjectProvider(NamedContextFactory clientFactory, String name, Class type) {
+ this.clientFactory = clientFactory;
+ this.name = name;
+ this.type = type;
+ }
+
+ @Override
+ public T getObject(Object... args) throws BeansException {
+ return delegate().getObject(args);
+ }
+
+ @Override
+ @Nullable
+ public T getIfAvailable() throws BeansException {
+ return delegate().getIfAvailable();
+ }
+
+ @Override
+ public T getIfAvailable(Supplier defaultSupplier) throws BeansException {
+ return delegate().getIfAvailable(defaultSupplier);
+ }
+
+ @Override
+ public void ifAvailable(Consumer dependencyConsumer) throws BeansException {
+ delegate().ifAvailable(dependencyConsumer);
+ }
+
+ @Override
+ @Nullable
+ public T getIfUnique() throws BeansException {
+ return delegate().getIfUnique();
+ }
+
+ @Override
+ public T getIfUnique(Supplier defaultSupplier) throws BeansException {
+ return delegate().getIfUnique(defaultSupplier);
+ }
+
+ @Override
+ public void ifUnique(Consumer dependencyConsumer) throws BeansException {
+ delegate().ifUnique(dependencyConsumer);
+ }
+
+ @Override
+ public Iterator iterator() {
+ return delegate().iterator();
+ }
+
+ @Override
+ public Stream stream() {
+ return delegate().stream();
+ }
+
+ @Override
+ public T getObject() throws BeansException {
+ return delegate().getObject();
+ }
+
+ @Override
+ public void forEach(Consumer super T> action) {
+ delegate().forEach(action);
+ }
+
+ @Override
+ public Spliterator spliterator() {
+ return delegate().spliterator();
+ }
+
+ @SuppressWarnings("unchecked")
+ private ObjectProvider delegate() {
+ if (this.provider == null) {
+ provider = this.clientFactory.getProvider(name, type);
+ }
+ return provider;
+ }
+}
diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java
index c6460b46..6287bc04 100644
--- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java
+++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java
@@ -11,10 +11,12 @@ import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.core.ResolvableType;
import org.springframework.core.env.MapPropertySource;
/**
@@ -26,6 +28,7 @@ import org.springframework.core.env.MapPropertySource;
* @author Spencer Gibb
* @author Dave Syer
*/
+//TODO: add javadoc
public abstract class NamedContextFactory
implements DisposableBean, ApplicationContextAware {
@@ -131,6 +134,35 @@ public abstract class NamedContextFactory ObjectProvider getLazyProvider(String name, Class type) {
+ return new ClientFactoryObjectProvider<>(this, name, type);
+ }
+
+ public ObjectProvider getProvider(String name, Class type) {
+ AnnotationConfigApplicationContext context = getContext(name);
+ return context.getBeanProvider(type);
+ }
+
+ public T getInstance(String name, Class> clazz, Class>... generics) {
+ ResolvableType type = ResolvableType.forClassWithGenerics(clazz, generics);
+ return getInstance(name, type);
+ }
+
+ @SuppressWarnings("unchecked")
+ public T getInstance(String name, ResolvableType type) {
+ AnnotationConfigApplicationContext context = getContext(name);
+ String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
+ type);
+ if (beanNames.length > 0) {
+ for (String beanName : beanNames) {
+ if (context.isTypeMatch(beanName, type)) {
+ return (T) context.getBean(beanName);
+ }
+ }
+ }
+ return null;
+ }
+
public Map getInstances(String name, Class type) {
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
diff --git a/spring-cloud-loadbalancer/pom.xml b/spring-cloud-loadbalancer/pom.xml
new file mode 100644
index 00000000..02b2b61e
--- /dev/null
+++ b/spring-cloud-loadbalancer/pom.xml
@@ -0,0 +1,88 @@
+
+
+ 4.0.0
+
+
+ org.springframework.cloud
+ spring-cloud-commons-parent
+ 2.1.0.BUILD-SNAPSHOT
+ ..
+
+ spring-cloud-loadbalancer
+ jar
+ Spring Cloud Load Balancer
+ Spring Cloud Balancer
+
+
+ org.springframework.cloud
+ spring-cloud-commons
+
+
+ org.springframework.cloud
+ spring-cloud-context
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+ true
+
+
+
+ org.apache.logging.log4j
+ log4j-to-slf4j
+
+
+
+
+ io.projectreactor
+ reactor-core
+
+
+ io.projectreactor.addons
+ reactor-extra
+
+ 3.2.0.M2
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-cache
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.cloud
+ spring-cloud-test-support
+ test
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+ test
+
+
+
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClient.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClient.java
new file mode 100644
index 00000000..ff84c880
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClient.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.annotation.AliasFor;
+
+/**
+ * Declarative configuration for a load balancer client. Add this annotation to any
+ * @Configuration and then inject a {@link LoadBalancerClientFactory} to access the
+ * client that is created.
+ *
+ * @author Dave Syer
+ */
+@Configuration
+@Import(LoadBalancerClientConfigurationRegistrar.class)
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface LoadBalancerClient {
+
+ /**
+ * Synonym for name (the name of the client)
+ *
+ * @see #name()
+ */
+ @AliasFor("name")
+ String value() default "";
+
+ /**
+ * The name of the load balancer client, uniquely identifying a set of client resources,
+ * including a load balancer.
+ */
+ @AliasFor("value")
+ String name() default "";
+
+ /**
+ * A custom @Configuration for the load balancer client. Can contain override
+ * @Bean definition for the pieces that make up the client.
+ *
+ * @see LoadBalancerClientConfiguration for the defaults
+ */
+ Class>[] configuration() default {};
+
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java
new file mode 100644
index 00000000..95eb8f35
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.annotation;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cache.CacheManager;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceSupplier;
+import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceSupplier;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceSupplier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+
+/**
+ * @author Spencer Gibb
+ */
+@Configuration
+@EnableConfigurationProperties
+public class LoadBalancerClientConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ServiceInstanceSupplier discoveryClientServiceInstanceSupplier(
+ DiscoveryClient discoveryClient, Environment env, ObjectProvider cacheManager) {
+ //TODO: bean post processor to enable caching?
+ DiscoveryClientServiceInstanceSupplier delegate = new DiscoveryClientServiceInstanceSupplier(discoveryClient, env);
+ if (cacheManager.getIfAvailable() != null) {
+ return new CachingServiceInstanceSupplier(delegate, cacheManager.getIfAvailable());
+ }
+ return delegate;
+ }
+
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationRegistrar.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationRegistrar.java
new file mode 100644
index 00000000..67c0fa07
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationRegistrar.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.annotation;
+
+import java.util.Map;
+
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Dave Syer
+ */
+public class LoadBalancerClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar {
+
+ @Override
+ public void registerBeanDefinitions(AnnotationMetadata metadata,
+ BeanDefinitionRegistry registry) {
+ Map attrs = metadata.getAnnotationAttributes(
+ LoadBalancerClients.class.getName(), true);
+ if (attrs != null && attrs.containsKey("value")) {
+ AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
+ for (AnnotationAttributes client : clients) {
+ registerClientConfiguration(registry, getClientName(client),
+ client.get("configuration"));
+ }
+ }
+ if (attrs != null && attrs.containsKey("defaultConfiguration")) {
+ String name;
+ if (metadata.hasEnclosingClass()) {
+ name = "default." + metadata.getEnclosingClassName();
+ } else {
+ name = "default." + metadata.getClassName();
+ }
+ registerClientConfiguration(registry, name,
+ attrs.get("defaultConfiguration"));
+ }
+ Map client = metadata.getAnnotationAttributes(
+ LoadBalancerClient.class.getName(), true);
+ String name = getClientName(client);
+ if (name != null) {
+ registerClientConfiguration(registry, name, client.get("configuration"));
+ }
+ }
+
+ private static String getClientName(Map client) {
+ if (client == null) {
+ return null;
+ }
+ String value = (String) client.get("value");
+ if (!StringUtils.hasText(value)) {
+ value = (String) client.get("name");
+ }
+ if (StringUtils.hasText(value)) {
+ return value;
+ }
+ throw new IllegalStateException(
+ "Either 'name' or 'value' must be provided in @LoadBalancerClient");
+ }
+
+ private static void registerClientConfiguration(BeanDefinitionRegistry registry,
+ Object name, Object configuration) {
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder
+ .genericBeanDefinition(LoadBalancerClientSpecification.class);
+ builder.addConstructorArgValue(name);
+ builder.addConstructorArgValue(configuration);
+ registry.registerBeanDefinition(name + ".LoadBalancerClientSpecification",
+ builder.getBeanDefinition());
+ }
+
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientSpecification.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientSpecification.java
new file mode 100644
index 00000000..b990e1a3
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientSpecification.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.annotation;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.springframework.cloud.context.named.NamedContextFactory;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.util.Assert;
+
+/**
+ * @author Dave Syer
+ */
+public class LoadBalancerClientSpecification implements NamedContextFactory.Specification {
+
+ private String name;
+
+ private Class>[] configuration;
+
+ public LoadBalancerClientSpecification() {
+ }
+
+ public LoadBalancerClientSpecification(String name, Class>[] configuration) {
+ Assert.hasText(name, "name must not be empty");
+ this.name = name;
+ Assert.notNull(configuration, "configuration must not be null");
+ this.configuration = configuration;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ Assert.hasText(name, "name must not be empty");
+ this.name = name;
+ }
+
+ public Class>[] getConfiguration() {
+ return configuration;
+ }
+
+ public void setConfiguration(Class>[] configuration) {
+ Assert.notNull(configuration, "configuration must not be null");
+ this.configuration = configuration;
+ }
+
+ @Override
+ public String toString() {
+ ToStringCreator to = new ToStringCreator(this);
+ to.append("name", name);
+ to.append("configuration", configuration);
+ return to.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LoadBalancerClientSpecification that = (LoadBalancerClientSpecification) o;
+ return Objects.equals(name, that.name) &&
+ Arrays.equals(configuration, that.configuration);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, configuration);
+ }
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClients.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClients.java
new file mode 100644
index 00000000..f7f94254
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClients.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * Convenience annotation that allows user to combine multiple @LoadBalancerClient
+ * annotations on a single class (including in Java 7).
+ *
+ * @author Dave Syer
+ */
+@Configuration
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE })
+@Documented
+@Import(LoadBalancerClientConfigurationRegistrar.class)
+public @interface LoadBalancerClients {
+
+ LoadBalancerClient[] value() default {};
+
+ /**
+ * {@link LoadBalancerClientConfigurationRegistrar} creates a {@link LoadBalancerClientSpecification}
+ * with this as an argument. These in turn are added as default contexts in {@link LoadBalancerClientFactory}.
+ * Configuration defined in these classes are used as defaults if values aren't defined via
+ * {@link LoadBalancerClient#configuration()}
+ */
+ Class>[] defaultConfiguration() default {};
+
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java
new file mode 100644
index 00000000..f078adf3
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.config;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Spencer Gibb
+ */
+@Configuration
+@LoadBalancerClients
+// @EnableCaching //TODO: how to enforce, or check conditions?
+// @AutoConfigureBefore(CacheAutoConfiguration.class)
+public class LoadBalancerAutoConfiguration {
+ private final ObjectProvider> configurations;
+
+ public LoadBalancerAutoConfiguration(ObjectProvider> configurations) {
+ this.configurations = configurations;
+ }
+
+ @Bean
+ public LoadBalancerClientFactory loadBalancerClientFactory() {
+ LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory();
+ clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList));
+ return clientFactory;
+ }
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceSupplier.java
new file mode 100644
index 00000000..8f8ad69c
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceSupplier.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.core;
+
+import java.util.List;
+
+import reactor.cache.CacheFlux;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+import org.springframework.cloud.client.ServiceInstance;
+
+/**
+ * @author Spencer Gibb
+ */
+public class CachingServiceInstanceSupplier implements ServiceInstanceSupplier {
+
+ public static final String SERVICE_INSTANCE_CACHE_NAME = CachingServiceInstanceSupplier.class.getSimpleName()+"Cache";
+
+ private final ServiceInstanceSupplier delegate;
+ private final Flux serviceInstances;
+
+ @SuppressWarnings("unchecked")
+ public CachingServiceInstanceSupplier(ServiceInstanceSupplier delegate, CacheManager cacheManager) {
+ this.delegate = delegate;
+ this.serviceInstances = CacheFlux.lookup(key -> {
+ Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME); //TODO: configurable cache name
+ List list = cache.get(key, List.class);
+ if (list == null || list.isEmpty()) {
+ return Mono.empty();
+ }
+ return Flux.fromIterable(list)
+ .materialize()
+ .collectList();
+ }, delegate.getServiceId())
+ .onCacheMissResume(this.delegate::get)
+ .andWriteWith((key, signals) -> Flux.fromIterable(signals)
+ .dematerialize()
+ .cast(ServiceInstance.class)
+ .collectList()
+ .doOnNext(instances -> {
+ Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
+ cache.put(key, instances);
+ })
+ .then());
+ }
+
+ @Override
+ public Flux get() {
+ return this.serviceInstances;
+ }
+
+ @Override
+ public String getServiceId() {
+ return this.delegate.getServiceId();
+ }
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/DiscoveryClientServiceInstanceSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/DiscoveryClientServiceInstanceSupplier.java
new file mode 100644
index 00000000..7b75a5b9
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/DiscoveryClientServiceInstanceSupplier.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.core;
+
+import java.util.List;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.core.env.Environment;
+
+import static org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory.PROPERTY_NAME;
+
+/**
+ * @author Spencer Gibb
+ */
+public class DiscoveryClientServiceInstanceSupplier implements ServiceInstanceSupplier {
+
+ private final DiscoveryClient delegate;
+ private final String serviceId;
+
+ public DiscoveryClientServiceInstanceSupplier(DiscoveryClient delegate, Environment environment) {
+ this.delegate = delegate;
+ serviceId = environment.getProperty(PROPERTY_NAME);
+ }
+
+ @Override
+ public Flux get() {
+ List instances = delegate.getInstances(serviceId);
+ return Flux.fromIterable(instances);
+ }
+
+ public String getServiceId() {
+ return this.serviceId;
+ }
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ReactorLoadBalancer.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ReactorLoadBalancer.java
new file mode 100644
index 00000000..3b3f0b6f
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ReactorLoadBalancer.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.core;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
+import org.springframework.cloud.client.loadbalancer.reactive.Request;
+import org.springframework.cloud.client.loadbalancer.reactive.Response;
+
+public interface ReactorLoadBalancer extends ReactiveLoadBalancer {
+ /**
+ * Choose the next server based on the load balancing algorithm
+ * @param request
+ * @return
+ */
+ Mono> choose(Request request);
+
+ default Mono> choose() {
+ return choose(REQUEST);
+ }
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RoundRobinLoadBalancer.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RoundRobinLoadBalancer.java
new file mode 100644
index 00000000..bd17c40e
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RoundRobinLoadBalancer.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.core;
+
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import reactor.core.publisher.Mono;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.reactive.DefaultResponse;
+import org.springframework.cloud.client.loadbalancer.reactive.EmptyResponse;
+import org.springframework.cloud.client.loadbalancer.reactive.Request;
+import org.springframework.cloud.client.loadbalancer.reactive.Response;
+
+/**
+ * @author Spencer Gibb
+ */
+public class RoundRobinLoadBalancer implements ReactorLoadBalancer {
+
+ private static final Log log = LogFactory.getLog(RoundRobinLoadBalancer.class);
+
+ private final AtomicInteger position;
+ private final ObjectProvider serviceInstanceSupplier;
+ private final String serviceId;
+
+ public RoundRobinLoadBalancer(String serviceId, ObjectProvider serviceInstanceSupplier) {
+ this(serviceId, serviceInstanceSupplier, new Random().nextInt(1000));
+ }
+
+ public RoundRobinLoadBalancer(String serviceId, ObjectProvider serviceInstanceSupplier,
+ int seedPosition) {
+ this.serviceId = serviceId;
+ this.serviceInstanceSupplier = serviceInstanceSupplier;
+ this.position = new AtomicInteger(seedPosition);
+ }
+
+ @Override
+ /**
+ * see original https://github.com/Netflix/ocelli/blob/master/ocelli-core/src/main/java/netflix/ocelli/loadbalancer/RoundRobinLoadBalancer.java
+ */
+ public Mono> choose(Request request) {
+ // TODO: move supplier to Request?
+ ServiceInstanceSupplier supplier = serviceInstanceSupplier.getIfAvailable();
+ return supplier.get().collectList().map(instances -> {
+ if (instances.isEmpty()) {
+ log.warn("No servers available for service: " + this.serviceId);
+ return new EmptyResponse();
+ }
+ // TODO: enforce order?
+ int pos = Math.abs(position.incrementAndGet());
+
+ ServiceInstance instance = instances.get(pos % instances.size());
+
+ return new DefaultResponse(instance);
+ });
+ }
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceSupplier.java
new file mode 100644
index 00000000..96ba3ede
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceSupplier.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.core;
+
+import java.util.function.Supplier;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.cloud.client.ServiceInstance;
+
+/**
+ * @author Spencer Gibb
+ */
+public interface ServiceInstanceSupplier extends Supplier> {
+
+ String getServiceId();
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java
new file mode 100644
index 00000000..a7623d08
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.support;
+
+import org.springframework.cloud.context.named.NamedContextFactory;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification;
+import org.springframework.core.env.Environment;
+
+/**
+ * A factory that creates client, load balancer and client configuration instances. It
+ * creates a Spring ApplicationContext per client name, and extracts the beans that it
+ * needs from there.
+ *
+ * @author Spencer Gibb
+ * @author Dave Syer
+ */
+public class LoadBalancerClientFactory extends NamedContextFactory {
+
+ public static final String NAMESPACE = "loadbalancer";
+ public static final String PROPERTY_NAME = NAMESPACE + ".client.name";
+
+ public LoadBalancerClientFactory() {
+ super(LoadBalancerClientConfiguration.class, NAMESPACE, PROPERTY_NAME);
+ }
+
+ public String getName(Environment environment) {
+ return environment.getProperty(PROPERTY_NAME);
+ }
+
+}
+
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/ServiceInstanceSuppliers.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/ServiceInstanceSuppliers.java
new file mode 100644
index 00000000..2c23bc8f
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/ServiceInstanceSuppliers.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.support;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceSupplier;
+import reactor.core.publisher.Flux;
+
+public class ServiceInstanceSuppliers {
+
+ public static ServiceInstanceSupplier from(String serviceId, ServiceInstance... instances) {
+ return new ServiceInstanceSupplier() {
+ @Override
+ public Flux get() {
+ return Flux.just(instances);
+ }
+
+ @Override
+ public String getServiceId() {
+ return serviceId;
+ }
+ };
+ }
+
+ public static ObjectProvider toProvider(String serviceId, ServiceInstance... instances) {
+ return new SimpleObjectProvider<>(from(serviceId, instances));
+ }
+}
diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/SimpleObjectProvider.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/SimpleObjectProvider.java
new file mode 100644
index 00000000..0b2d0366
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/SimpleObjectProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.support;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.ObjectProvider;
+
+public class SimpleObjectProvider implements ObjectProvider {
+
+ private final T object;
+
+ public SimpleObjectProvider(T object) {
+ this.object = object;
+ }
+
+ @Override
+ public T getObject(Object... args) throws BeansException {
+ return this.object;
+ }
+
+ @Override
+ public T getIfAvailable() throws BeansException {
+ return this.object;
+ }
+
+ @Override
+ public T getIfUnique() throws BeansException {
+ return this.object;
+ }
+
+ @Override
+ public T getObject() throws BeansException {
+ return this.object;
+ }
+}
diff --git a/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories b/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000..b12b173d
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,4 @@
+# AutoConfiguration
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration
+
diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerTest.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerTest.java
new file mode 100644
index 00000000..ac8e8a96
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.loadbalancer.core;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cloud.client.DefaultServiceInstance;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.reactive.CompletionContext;
+import org.springframework.cloud.client.loadbalancer.reactive.CompletionContext.Status;
+import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
+import org.springframework.cloud.client.loadbalancer.reactive.Response;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+import org.springframework.cloud.loadbalancer.support.ServiceInstanceSuppliers;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.env.Environment;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Spencer Gibb
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class LoadBalancerTest {
+
+ @Autowired
+ private LoadBalancerClientFactory clientFactory;
+
+ @Test
+ public void roundRobbinLoadbalancerWorks() {
+ ReactiveLoadBalancer reactiveLoadBalancer = this.clientFactory
+ .getInstance("myservice", ReactiveLoadBalancer.class, ServiceInstance.class);
+
+ assertThat(reactiveLoadBalancer).isInstanceOf(RoundRobinLoadBalancer.class);
+ assertThat(reactiveLoadBalancer).isInstanceOf(ReactorLoadBalancer.class);
+ ReactorLoadBalancer loadBalancer = (ReactorLoadBalancer) reactiveLoadBalancer;
+
+ //order dependent on seedPosition -1 of RoundRobinLoadBalancer
+ List hosts = Arrays.asList("a.host", "c.host", "b.host-secure", "a.host");
+
+ assertLoadBalancer(loadBalancer, hosts);
+ }
+
+ private void assertLoadBalancer(ReactorLoadBalancer loadBalancer, List hosts) {
+ for (String host : hosts) {
+ Mono> source = loadBalancer.choose();
+ StepVerifier.create(source).consumeNextWith(response -> {
+ assertThat(response).isNotNull();
+ assertThat(response.hasServer()).isTrue();
+
+ ServiceInstance instance = response.getServer();
+ assertThat(instance).isNotNull();
+ assertThat(instance.getHost())
+ .as("instance host is incorrent %s", host)
+ .isEqualTo(host);
+
+ if (host.contains("secure")) {
+ assertThat(instance.isSecure()).isTrue();
+ } else {
+ assertThat(instance.isSecure()).isFalse();
+ }
+
+ response.onComplete(new CompletionContext(Status.SUCCESSS));
+ }).verifyComplete();
+ }
+ }
+
+ @Test
+ public void emptyHosts() {
+ ResolvableType type = ResolvableType.forClassWithGenerics(ReactorLoadBalancer.class, ServiceInstance.class);
+ ReactorLoadBalancer loadBalancer = this.clientFactory.getInstance("unknownservice", type);
+
+ assertThat(loadBalancer).isInstanceOf(RoundRobinLoadBalancer.class);
+
+ Mono> source = loadBalancer.choose();
+ StepVerifier.create(source).consumeNextWith(response -> {
+ assertThat(response).isNotNull();
+ assertThat(response.hasServer()).isFalse();
+ }).verifyComplete();
+ }
+
+ @Test
+ public void staticConfigurationWorks() {
+ String serviceId = "test1";
+ RoundRobinLoadBalancer loadBalancer = new RoundRobinLoadBalancer(serviceId,
+ ServiceInstanceSuppliers.toProvider(serviceId, instance(serviceId, "1.host", false),
+ instance(serviceId, "2.host-secure", true)),
+ -1);
+ assertLoadBalancer(loadBalancer, Arrays.asList("1.host", "2.host-secure"));
+ }
+
+ private DefaultServiceInstance instance(String serviceId, String host, boolean secure) {
+ return new DefaultServiceInstance(serviceId, host, 80, secure);
+ }
+
+ @EnableAutoConfiguration
+ @SpringBootConfiguration
+ @LoadBalancerClients({
+ @LoadBalancerClient(name = "myservice", configuration = MyServiceConfig.class),
+ @LoadBalancerClient(name = "unknownservice", configuration = MyServiceConfig.class),
+ })
+ @EnableCaching
+ protected static class Config { }
+
+ protected static class MyServiceConfig {
+ @Bean
+ public RoundRobinLoadBalancer roundRobinContextLoadBalancer(LoadBalancerClientFactory clientFactory, Environment env) {
+ String serviceId = clientFactory.getName(env);
+ return new RoundRobinLoadBalancer(serviceId,
+ clientFactory.getLazyProvider(serviceId, ServiceInstanceSupplier.class),
+ -1);
+ }
+ }
+}
diff --git a/spring-cloud-loadbalancer/src/test/resources/application.yml b/spring-cloud-loadbalancer/src/test/resources/application.yml
new file mode 100644
index 00000000..41b22c67
--- /dev/null
+++ b/spring-cloud-loadbalancer/src/test/resources/application.yml
@@ -0,0 +1,36 @@
+spring:
+ cloud:
+ discovery:
+ client:
+ simple:
+ instances:
+ myservice:
+ -
+ service-id: myservice
+ uri: http://a.host
+ -
+ service-id: myservice
+ uri: http://c.host
+ -
+ service-id: myservice
+ uri: https://b.host-secure
+ anotherservice:
+ -
+ service-id: myservice
+ uri: http://d.host
+ -
+ service-id: myservice
+ uri: http://f.host
+ -
+ service-id: myservice
+ uri: https://e.host
+ thirdservice:
+ -
+ service-id: myservice
+ uri: http://g.host
+ -
+ service-id: myservice
+ uri: http://h.host
+ -
+ service-id: myservice
+ uri: https://i.host
\ No newline at end of file