From 19bab039a9eebbba08aa9a407adee4642e28d3bf Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Mon, 9 Sep 2019 17:49:46 +0200 Subject: [PATCH] Gh 586 loadbalancer starter (#599) * Add loadbalancer starter. Add property to set BlockingLoadBalancerClient as default non-reactive loadbalancer. Start working on auto-enabling caching. * Automatically enable caching. Fixes gh-585. * Add tests. * Add reference docs. * Change property name. --- .../main/asciidoc/spring-cloud-commons.adoc | 24 ++++--- pom.xml | 1 + spring-cloud-commons-dependencies/pom.xml | 5 ++ ...itional-spring-configuration-metadata.json | 6 ++ ...ngLoadBalancerClientAutoConfiguration.java | 33 +++++++-- .../LoadBalancerCacheAutoConfiguration.java | 44 ++++++++++++ .../main/resources/META-INF/spring.factories | 3 +- ...adBalancerCacheAutoConfigurationTests.java | 70 +++++++++++++++++++ spring-cloud-starter-loadbalancer/pom.xml | 39 +++++++++++ .../main/resources/META-INF/spring.provides | 1 + 10 files changed, 209 insertions(+), 17 deletions(-) create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfiguration.java create mode 100644 spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfigurationTests.java create mode 100644 spring-cloud-starter-loadbalancer/pom.xml create mode 100644 spring-cloud-starter-loadbalancer/src/main/resources/META-INF/spring.provides diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index 2d176f94..568246b9 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -371,12 +371,13 @@ See {githubroot}/spring-cloud-netflix/blob/master/spring-cloud-netflix-ribbon/sr IMPORTANT: In order to use a load-balanced `RestTemplate`, you need to have a load-balancer implementation in your classpath. The recommended implementation is `BlockingLoadBalancerClient` -- add `org.springframework.cloud:spring-cloud-loadbalancer` in order to use it. +- add `org.springframework.cloud:spring-cloud-starter-loadbalancer` in order to use it. The `RibbonLoadBalancerClient` also can be used, but it's now under maintenance and we do not recommend adding it to new projects. -WARNING: If you want to use `BlockingLoadBalancerClient`, make sure you do not have -`RibbonLoadBalancerClient` in the project classpath, as for backward compatibility reasons, it will be used by default. +WARNING: If you have both `RibbonLoadBalancerClient` and `BlockingLoadBalancerClient`, in order to +preserve backward compatibility, `RibbonLoadBalancerClient` will be used by default. In order +to override it, you can set the property `spring.cloud.loadbalancer.ribbon.enabled` to `false`. === Spring WebClient as a Load Balancer Client @@ -411,17 +412,18 @@ The Ribbon client is used to create a full physical address. IMPORTANT: If you want to use a `@LoadBalanced WebClient.Builder`, you need to have a loadbalancer implementation in the classpath. It is recommended that you add the -`org.springframework.cloud:spring-cloud-loadbalancer` dependency to your project. +`org.springframework.cloud:spring-cloud-starter-loadbalancer` dependency to your project. Then, `ReactiveLoadBalancer` will be used underneath. Alternatively, this functionality will also work with spring-cloud-starter-netflix-ribbon, but the request will be handled by a non-reactive `LoadBalancerClient` under the hood. Additionally, -spring-cloud-starter-netflix-ribbon is already in maintenance mode, so we do not recommned +spring-cloud-starter-netflix-ribbon is already in maintenance mode, so we do not recommend adding it to new projects. -TIP: The `ReactorLoadBalancer` used underneath supports caching. If `cacheManager` is detected, -cached version of `ServiceInstanceSupplier` will be used. If not, we will retrieve instances -from discovery service without caching them. We recommend https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html[enabling caching] in your project -if you use `ReactiveLoadBalancer`. +IMPORTANT: In order to make use of the more efficient cached version of `ServiceInstanceSupplier`, + `spring-cloud-starter-loadbalancer` will *enable caching* by default. +https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html[Spring Boot Caching] +mechanism will be used under the hood. If you don't want caching to be used, you can set +the value of `spring.cache.type` to `none`. ==== Retrying Failed Requests @@ -541,7 +543,7 @@ TIP: If you see errors such as `java.lang.IllegalArgumentException: Can not set ==== Spring WebFlux WebClient with Reactive Load Balancer `WebClient` can be configured to use the `ReactiveLoadBalancer`. -If you add `org.springframework.cloud:spring-cloud-loadbalancer` to your project, +If you add `org.springframework.cloud:spring-cloud-starter-loadbalancer` to your project, `ReactorLoadBalancerExchangeFilterFunction` is auto-configured if `spring-webflux` is on the classpath. The following example shows how to configure a `WebClient` to use reactive load balancer under the hood: @@ -568,7 +570,7 @@ The `ReactorLoadBalancerClient` is used to create a full physical address. ==== Spring WebFlux WebClient with non-reactive Load Balancer Client -If you you don't have `org.springframework.cloud:spring-cloud-loadbalancer` in your project, +If you you don't have `org.springframework.cloud:spring-cloud-starter-loadbalancer` in your project, but you do have spring-cloud-starter-netflix-ribbon, you can still use `WebClient` with `LoadBalancerClient`. `LoadBalancerExchangeFilterFunction` will be auto-configured if `spring-webflux` is on the classpath. Please note, however, that this is uses a non-reactive client under the hood. diff --git a/pom.xml b/pom.xml index c79c132b..3295eabb 100644 --- a/pom.xml +++ b/pom.xml @@ -155,6 +155,7 @@ spring-cloud-commons spring-cloud-loadbalancer spring-cloud-starter + spring-cloud-starter-loadbalancer docs diff --git a/spring-cloud-commons-dependencies/pom.xml b/spring-cloud-commons-dependencies/pom.xml index def8023a..2bb8d6aa 100644 --- a/spring-cloud-commons-dependencies/pom.xml +++ b/spring-cloud-commons-dependencies/pom.xml @@ -54,6 +54,11 @@ spring-cloud-starter ${project.version} + + org.springframework.cloud + spring-cloud-starter-loadbalancer + ${project.version} + diff --git a/spring-cloud-commons/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-commons/src/main/resources/META-INF/additional-spring-configuration-metadata.json index dd3b1a41..b8499d25 100644 --- a/spring-cloud-commons/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-commons/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -47,6 +47,12 @@ "name": "management.endpoint.env.post.enabled", "description": "Enables writable environment endpoint.", "type": "java.lang.Boolean" + }, + { + "defaultValue": true, + "name": "spring.cloud.loadbalancer.ribbon.enabled", + "description": "Causes `RibbonLoadBalancerClient` to be used by default.", + "type": "java.lang.Boolean" } ] } diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java index 1aaee4ad..af4a58b4 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java @@ -23,17 +23,20 @@ import org.apache.commons.logging.LogFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.web.client.RestTemplate; /** @@ -60,13 +63,32 @@ public class BlockingLoadBalancerClientAutoConfiguration { @Bean @ConditionalOnBean(LoadBalancerClientFactory.class) @ConditionalOnClass(RestTemplate.class) - @ConditionalOnMissingBean - @ConditionalOnMissingClass("org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient") - public LoadBalancerClient loadBalancerClient( + @Conditional(OnNoRibbonDefaultCondition.class) + @Primary + public LoadBalancerClient blockingLoadBalancerClient( LoadBalancerClientFactory loadBalancerClientFactory) { return new BlockingLoadBalancerClient(loadBalancerClientFactory); } + private static final class OnNoRibbonDefaultCondition extends AnyNestedCondition { + + private OnNoRibbonDefaultCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled", + havingValue = "false") + static class RibbonNotEnabled { + + } + + @ConditionalOnMissingClass("org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient") + static class RibbonLoadBalancerNotPresent { + + } + + } + } class RibbonWarnLogger { @@ -79,7 +101,8 @@ class RibbonWarnLogger { LOG.warn( "You already have RibbonLoadBalancerClient on your classpath. It will be used by default. To use " + BlockingLoadBalancerClient.class.getSimpleName() - + " remove spring-cloud-starter-netflix-ribbon from your project."); + + " set the value of `spring.cloud.loadbalancer.ribbon.enabled` to `false` or " + + "remove spring-cloud-starter-netflix-ribbon from your project."); } } diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfiguration.java new file mode 100644 index 00000000..2e5ccc7e --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.loadbalancer.config; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheAspectSupport; +import org.springframework.context.annotation.Configuration; + +/** + * An AutoConfiguration that automatically enables caching when when Spring Boot and + * Spring Framework Cache support classes are present. + * + * @author Olga Maciaszek-Sharma + * @see CacheManager + * @see CacheAutoConfiguration + * @see CacheAspectSupport + */ +@Configuration +@ConditionalOnClass({ CacheManager.class, CacheAutoConfiguration.class }) +@ConditionalOnMissingBean(CacheAspectSupport.class) +@EnableCaching +@AutoConfigureBefore(CacheAutoConfiguration.class) +public class LoadBalancerCacheAutoConfiguration { + +} diff --git a/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories b/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories index 9fa6cb80..3e9c647b 100644 --- a/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories @@ -1,4 +1,5 @@ # AutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration,\ -org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration +org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration,\ +org.springframework.cloud.loadbalancer.config.LoadBalancerCacheAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfigurationTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfigurationTests.java new file mode 100644 index 00000000..430f9d3a --- /dev/null +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfigurationTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.loadbalancer.config; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.cache.CacheManager; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LoadBalancerCacheAutoConfiguration}. + * + * @author Olga Maciaszek-Sharma + */ + +class LoadBalancerCacheAutoConfigurationTests { + + @Test + void shouldAutoEnableCaching() { + AnnotationConfigApplicationContext context = setup(""); + assertThat(context.getBeansOfType(CacheManager.class)).isNotEmpty(); + assertThat(context.getBeansOfType(CacheManager.class).get("cacheManager")) + .isNotInstanceOf(NoOpCacheManager.class); + } + + @Test + void shouldUseNoOpCacheIfCacheTypeNone() { + AnnotationConfigApplicationContext context = setup("spring.cache.type=none"); + assertThat(context.getBeansOfType(CacheManager.class)).isNotEmpty(); + assertThat(context.getBeansOfType(CacheManager.class).get("cacheManager")) + .isInstanceOf(NoOpCacheManager.class); + } + + private AnnotationConfigApplicationContext setup(String property) { + List config = new ArrayList<>(); + config.add(LoadBalancerCacheAutoConfiguration.class); + config.add(CacheAutoConfiguration.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + if (StringUtils.hasText(property)) { + TestPropertyValues.of(property).applyTo(context); + } + context.register(config.toArray(new Class[0])); + context.refresh(); + return context; + } + +} diff --git a/spring-cloud-starter-loadbalancer/pom.xml b/spring-cloud-starter-loadbalancer/pom.xml new file mode 100644 index 00000000..b579826c --- /dev/null +++ b/spring-cloud-starter-loadbalancer/pom.xml @@ -0,0 +1,39 @@ + + + + org.springframework.cloud + spring-cloud-commons-parent + 2.2.0.BUILD-SNAPSHOT + .. + + 4.0.0 + + spring-cloud-starter-loadbalancer + spring-cloud-starter-loadbalancer + Spring Cloud Starter LoadBalancer + https://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + https://www.spring.io + + + ${basedir}/../.. + + + + + org.springframework.cloud + spring-cloud-starter + + + org.springframework.cloud + spring-cloud-loadbalancer + + + org.springframework.boot + spring-boot-starter-cache + + + \ No newline at end of file diff --git a/spring-cloud-starter-loadbalancer/src/main/resources/META-INF/spring.provides b/spring-cloud-starter-loadbalancer/src/main/resources/META-INF/spring.provides new file mode 100644 index 00000000..4c34e82b --- /dev/null +++ b/spring-cloud-starter-loadbalancer/src/main/resources/META-INF/spring.provides @@ -0,0 +1 @@ +provides: spring-cloud-loadbalancer \ No newline at end of file