From f661105e63c631fbf1b8f3672e2913fd415129be Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Wed, 6 Nov 2019 19:06:39 +0100 Subject: [PATCH] Fix gh 177 add sc loadbalancer support (#239) * Add FeignBlockingLoadBalancerClient, configurations for default, apache httpclient and okhttp setup. Add autoconfiguration. Init fix for gh-177. * Fix SC loadbalancer dependencies. * Fix generating Feign response when instance not found. * Fix configuration. * Add tests for FeignBlockingLoadBalancerClient and FeignLoadBalancerAutoConfiguration. * Fix checkstyle and formatting. * Refactor, add docs and javadocs. * Fix author name. * Fix docs after code review. --- .../main/asciidoc/spring-cloud-openfeign.adoc | 25 ++- spring-cloud-openfeign-core/pom.xml | 5 + .../HttpClientFeignConfiguration.java | 123 +++++++++++++++ .../OkHttpFeignConfiguration.java | 79 ++++++++++ ...DefaultFeignLoadBalancerConfiguration.java | 43 ++++++ .../FeignBlockingLoadBalancerClient.java | 86 +++++++++++ .../FeignLoadBalancerAutoConfiguration.java | 57 +++++++ ...pClientFeignLoadBalancerConfiguration.java | 55 +++++++ .../OkHttpFeignLoadBalancerConfiguration.java | 54 +++++++ .../FeignRibbonClientAutoConfiguration.java | 4 + ...pClientFeignLoadBalancedConfiguration.java | 98 +----------- .../OkHttpFeignLoadBalancedConfiguration.java | 53 +------ .../main/resources/META-INF/spring.factories | 3 +- .../FeignBlockingLoadBalancerClientTests.java | 145 ++++++++++++++++++ ...ignLoadBalancerAutoConfigurationTests.java | 113 ++++++++++++++ spring-cloud-starter-openfeign/pom.xml | 5 + 16 files changed, 797 insertions(+), 151 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/HttpClientFeignConfiguration.java create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/OkHttpFeignConfiguration.java create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/DefaultFeignLoadBalancerConfiguration.java create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClientFeignLoadBalancerConfiguration.java create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/OkHttpFeignLoadBalancerConfiguration.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClientTests.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfigurationTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index dda56830..ec4726ac 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -9,7 +9,13 @@ include::intro.adoc[] [[spring-cloud-feign]] == Declarative REST Client: Feign -https://github.com/Netflix/feign[Feign] is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same `HttpMessageConverters` used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client when using Feign. +https://github.com/OpenFeign/feign[Feign] is a declarative web service client. +It makes writing web service clients easier. +To use Feign create an interface and annotate it. +It has pluggable annotation support including Feign annotations and JAX-RS annotations. +Feign also supports pluggable encoders and decoders. +Spring Cloud adds support for Spring MVC annotations and for using the same `HttpMessageConverters` used by default in Spring Web. +Spring Cloud integrates Ribbon and Eureka, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign. [[netflix-feign-starter]] === How to Include Feign @@ -46,22 +52,24 @@ public interface StoreClient { } ---- -In the `@FeignClient` annotation the String value ("stores" above) is -an arbitrary client name, which is used to create a Ribbon load -balancer (see <>). You can also specify a URL using the `url` attribute +In the `@FeignClient` annotation the String value ("stores" above) is an arbitrary client name, which is used to create either a https://github.com/Netflix/ribbon[Ribbon] load-balancer (see <>) or https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java[Spring Cloud LoadBalancer]. +You can also specify a URL using the `url` attribute (absolute value or just a hostname). The name of the bean in the application context is the fully qualified name of the interface. To specify your own alias value you can use the `qualifier` value of the `@FeignClient` annotation. -The Ribbon client above will want to discover the physical addresses +The load-balancer client above will want to discover the physical addresses for the "stores" service. If your application is a Eureka client then it will resolve the service in the Eureka service registry. If you don't want to use Eureka, you can simply configure a list of servers in your external configuration (see <>). +WARNING: In order to maintain backward compatibility, is used as the default load-balancer implementation. +However, Spring Cloud Netflix Ribbon is now in maintenance mode, so we recommend using Spring Cloud LoadBalancer instead. +To do this, set the value of `spring.cloud.loadbalancer.ribbon.enabled` to `false`. + [[spring-cloud-feign-overriding-defaults]] === Overriding Feign Defaults @@ -109,7 +117,10 @@ Spring Cloud Netflix provides the following beans by default for feign (`BeanTyp * `Logger` feignLogger: `Slf4jLogger` * `Contract` feignContract: `SpringMvcContract` * `Feign.Builder` feignBuilder: `HystrixFeign.Builder` -* `Client` feignClient: if Ribbon is enabled it is a `LoadBalancerFeignClient`, otherwise the default feign client is used. +* `Client` feignClient: if Ribbon is in the classpath and is enabled it is a `LoadBalancerFeignClient`, otherwise if Spring Cloud LoadBalancer is in the classpath, `FeignBlockingLoadBalancerClient` is used. +If none of them is in the classpath, the default feign client is used. + +NOTE: `spring-cloud-starter-openfeign` contains both `spring-cloud-starter-netflix-ribbon` and `spring-cloud-starter-loadbalancer`. The OkHttpClient and ApacheHttpClient feign clients can be used by setting `feign.okhttp.enabled` or `feign.httpclient.enabled` to `true`, respectively, and having them on the classpath. You can customize the HTTP client used by providing a bean of either `org.apache.http.impl.client.CloseableHttpClient` when using Apache or `okhttp3.OkHttpClient` when using OK HTTP. diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index d6d78e0d..81cc0b8c 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -231,6 +231,11 @@ 0.10.0 test + + org.springframework.cloud + spring-cloud-loadbalancer + true + diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/HttpClientFeignConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/HttpClientFeignConfiguration.java new file mode 100644 index 00000000..65dbdd23 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/HttpClientFeignConfiguration.java @@ -0,0 +1,123 @@ +/* + * Copyright 2013-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.openfeign.clientconfig; + +import java.util.Timer; +import java.util.TimerTask; + +import javax.annotation.PreDestroy; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory; +import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory; +import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Default configuration for {@link CloseableHttpClient}. + * + * @author Ryan Baxter + * @author Marcin Grzejszczak + * @author Spencer Gibb + * @author Olga Maciaszek-Sharma + */ +@Configuration +@ConditionalOnMissingBean(CloseableHttpClient.class) +public class HttpClientFeignConfiguration { + + private final Timer connectionManagerTimer = new Timer( + "FeignApacheHttpClientConfiguration.connectionManagerTimer", true); + + private CloseableHttpClient httpClient; + + @Autowired(required = false) + private RegistryBuilder registryBuilder; + + @Bean + @ConditionalOnMissingBean(HttpClientConnectionManager.class) + public HttpClientConnectionManager connectionManager( + ApacheHttpClientConnectionManagerFactory connectionManagerFactory, + FeignHttpClientProperties httpClientProperties) { + final HttpClientConnectionManager connectionManager = connectionManagerFactory + .newConnectionManager(httpClientProperties.isDisableSslValidation(), + httpClientProperties.getMaxConnections(), + httpClientProperties.getMaxConnectionsPerRoute(), + httpClientProperties.getTimeToLive(), + httpClientProperties.getTimeToLiveUnit(), this.registryBuilder); + this.connectionManagerTimer.schedule(new TimerTask() { + @Override + public void run() { + connectionManager.closeExpiredConnections(); + } + }, 30000, httpClientProperties.getConnectionTimerRepeat()); + return connectionManager; + } + + @Bean + @ConditionalOnProperty(value = "feign.compression.response.enabled", + havingValue = "true") + public CloseableHttpClient customHttpClient( + HttpClientConnectionManager httpClientConnectionManager, + FeignHttpClientProperties httpClientProperties) { + HttpClientBuilder builder = HttpClientBuilder.create().disableCookieManagement() + .useSystemProperties(); + this.httpClient = createClient(builder, httpClientConnectionManager, + httpClientProperties); + return this.httpClient; + } + + @Bean + @ConditionalOnProperty(value = "feign.compression.response.enabled", + havingValue = "false", matchIfMissing = true) + public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory, + HttpClientConnectionManager httpClientConnectionManager, + FeignHttpClientProperties httpClientProperties) { + this.httpClient = createClient(httpClientFactory.createBuilder(), + httpClientConnectionManager, httpClientProperties); + return this.httpClient; + } + + private CloseableHttpClient createClient(HttpClientBuilder builder, + HttpClientConnectionManager httpClientConnectionManager, + FeignHttpClientProperties httpClientProperties) { + RequestConfig defaultRequestConfig = RequestConfig.custom() + .setConnectTimeout(httpClientProperties.getConnectionTimeout()) + .setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build(); + CloseableHttpClient httpClient = builder + .setDefaultRequestConfig(defaultRequestConfig) + .setConnectionManager(httpClientConnectionManager).build(); + return httpClient; + } + + @PreDestroy + public void destroy() throws Exception { + this.connectionManagerTimer.cancel(); + if (this.httpClient != null) { + this.httpClient.close(); + } + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/OkHttpFeignConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/OkHttpFeignConfiguration.java new file mode 100644 index 00000000..8d7006d4 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/OkHttpFeignConfiguration.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-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.openfeign.clientconfig; + +import java.util.concurrent.TimeUnit; + +import javax.annotation.PreDestroy; + +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; +import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; +import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Defualt configuration for {@link OkHttpClient}. + * + * @author Ryan Baxter + * @author Marcin Grzejszczak + * @author Spencer Gibb + * @author Olga Maciaszek-Sharma + */ +@Configuration +@ConditionalOnMissingBean(okhttp3.OkHttpClient.class) +public class OkHttpFeignConfiguration { + + private okhttp3.OkHttpClient okHttpClient; + + @Bean + @ConditionalOnMissingBean(ConnectionPool.class) + public ConnectionPool httpClientConnectionPool( + FeignHttpClientProperties httpClientProperties, + OkHttpClientConnectionPoolFactory connectionPoolFactory) { + Integer maxTotalConnections = httpClientProperties.getMaxConnections(); + Long timeToLive = httpClientProperties.getTimeToLive(); + TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit(); + return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit); + } + + @Bean + public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, + ConnectionPool connectionPool, + FeignHttpClientProperties httpClientProperties) { + Boolean followRedirects = httpClientProperties.isFollowRedirects(); + Integer connectTimeout = httpClientProperties.getConnectionTimeout(); + this.okHttpClient = httpClientFactory + .createBuilder(httpClientProperties.isDisableSslValidation()) + .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .followRedirects(followRedirects).connectionPool(connectionPool).build(); + return this.okHttpClient; + } + + @PreDestroy + public void destroy() { + if (this.okHttpClient != null) { + this.okHttpClient.dispatcher().executorService().shutdown(); + this.okHttpClient.connectionPool().evictAll(); + } + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/DefaultFeignLoadBalancerConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/DefaultFeignLoadBalancerConfiguration.java new file mode 100644 index 00000000..a576deb0 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/DefaultFeignLoadBalancerConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-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.openfeign.loadbalancer; + +import feign.Client; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration instantiating a {@link BlockingLoadBalancerClient}-based {@link Client} + * object that uses {@link Client.Default} under the hood. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.0 + */ +@Configuration +class DefaultFeignLoadBalancerConfiguration { + + @Bean + @ConditionalOnMissingBean + public Client feignClient(BlockingLoadBalancerClient loadBalancerClient) { + return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), + loadBalancerClient); + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java new file mode 100644 index 00000000..ef61e1e2 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013-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.openfeign.loadbalancer; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; + +import feign.Client; +import feign.Request; +import feign.Response; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +/** + * A {@link Client} implementation that uses {@link BlockingLoadBalancerClient} to select + * a {@link ServiceInstance} to use while resolving the request host. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.0 + */ +class FeignBlockingLoadBalancerClient implements Client { + + private static final Log LOG = LogFactory + .getLog(FeignBlockingLoadBalancerClient.class); + + private final Client delegate; + + private final BlockingLoadBalancerClient loadBalancerClient; + + FeignBlockingLoadBalancerClient(Client delegate, + BlockingLoadBalancerClient loadBalancerClient) { + this.delegate = delegate; + this.loadBalancerClient = loadBalancerClient; + } + + @Override + public Response execute(Request request, Request.Options options) throws IOException { + final URI originalUri = URI.create(request.url()); + String serviceId = originalUri.getHost(); + Assert.state(serviceId != null, + "Request URI does not contain a valid hostname: " + originalUri); + ServiceInstance instance = loadBalancerClient.choose(serviceId); + if (instance == null) { + String message = "Load balancer does not contain an instance for the service " + + serviceId; + if (LOG.isWarnEnabled()) { + LOG.warn(message); + } + return Response.builder().request(request) + .status(HttpStatus.SERVICE_UNAVAILABLE.value()) + .body(message, StandardCharsets.UTF_8).build(); + } + String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri) + .toString(); + Request newRequest = Request.create(request.httpMethod(), reconstructedUrl, + request.headers(), request.requestBody()); + + return delegate.execute(newRequest, options); + } + + // Visible for tests + Client getDelegate() { + return delegate; + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java new file mode 100644 index 00000000..9ee0c3c2 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-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.openfeign.loadbalancer; + +import feign.Client; +import feign.Feign; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration; +import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * An autoconfiguration that instantiates {@link BlockingLoadBalancerClient}-based + * implementations of {@link Client}. In order to use this load-balancing mechanism, the + * Ribbon-based implementation has to be disabled by setting + * spring.cloud.loadbalancer.ribbon.enabled to true. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.0 + */ +@ConditionalOnClass(Feign.class) +@ConditionalOnBean(BlockingLoadBalancerClient.class) +@AutoConfigureBefore(FeignAutoConfiguration.class) +@AutoConfigureAfter(FeignRibbonClientAutoConfiguration.class) +@EnableConfigurationProperties(FeignHttpClientProperties.class) +@Configuration +// Order is important here, last should be the default, first should be optional +// see +// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653 +@Import({ HttpClientFeignLoadBalancerConfiguration.class, + OkHttpFeignLoadBalancerConfiguration.class, + DefaultFeignLoadBalancerConfiguration.class }) +class FeignLoadBalancerAutoConfiguration { + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClientFeignLoadBalancerConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClientFeignLoadBalancerConfiguration.java new file mode 100644 index 00000000..c8adaa41 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClientFeignLoadBalancerConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-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.openfeign.loadbalancer; + +import feign.Client; +import feign.httpclient.ApacheHttpClient; +import org.apache.http.client.HttpClient; + +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.ConditionalOnProperty; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * Configuration instantiating a {@link BlockingLoadBalancerClient}-based {@link Client} + * object that uses {@link ApacheHttpClient} under the hood. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.0 + */ +@Configuration +@ConditionalOnClass(ApacheHttpClient.class) +@ConditionalOnBean(BlockingLoadBalancerClient.class) +@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true) +@Import(HttpClientFeignConfiguration.class) +class HttpClientFeignLoadBalancerConfiguration { + + @Bean + @ConditionalOnMissingBean + public Client feignClient(BlockingLoadBalancerClient loadBalancerClient, + HttpClient httpClient) { + ApacheHttpClient delegate = new ApacheHttpClient(httpClient); + return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient); + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/OkHttpFeignLoadBalancerConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/OkHttpFeignLoadBalancerConfiguration.java new file mode 100644 index 00000000..9091252b --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/OkHttpFeignLoadBalancerConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-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.openfeign.loadbalancer; + +import feign.Client; +import feign.okhttp.OkHttpClient; + +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.ConditionalOnProperty; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.cloud.openfeign.clientconfig.OkHttpFeignConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * Configuration instantiating a {@link BlockingLoadBalancerClient}-based {@link Client} + * object that uses {@link OkHttpClient} under the hood. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.0 + */ +@Configuration +@ConditionalOnClass(OkHttpClient.class) +@ConditionalOnProperty("feign.okhttp.enabled") +@ConditionalOnBean(BlockingLoadBalancerClient.class) +@Import(OkHttpFeignConfiguration.class) +class OkHttpFeignLoadBalancerConfiguration { + + @Bean + @ConditionalOnMissingBean + public Client feignClient(okhttp3.OkHttpClient okHttpClient, + BlockingLoadBalancerClient loadBalancerClient) { + OkHttpClient delegate = new OkHttpClient(okHttpClient); + return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient); + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientAutoConfiguration.java index 02560c5f..9442f27e 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientAutoConfiguration.java @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.AutoConfigureBefore; 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.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.netflix.ribbon.SpringClientFactory; @@ -39,8 +40,11 @@ import org.springframework.context.annotation.Primary; * load balancer. * * @author Dave Syer + * @author Olga Maciaszek-Sharma */ @ConditionalOnClass({ ILoadBalancer.class, Feign.class }) +@ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled", + matchIfMissing = true) @Configuration @AutoConfigureBefore(FeignAutoConfiguration.class) @EnableConfigurationProperties({ FeignHttpClientProperties.class }) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/HttpClientFeignLoadBalancedConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/HttpClientFeignLoadBalancedConfiguration.java index 6ca9b237..d95709e9 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/HttpClientFeignLoadBalancedConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/HttpClientFeignLoadBalancedConfiguration.java @@ -16,37 +16,27 @@ package org.springframework.cloud.openfeign.ribbon; -import java.util.Timer; -import java.util.TimerTask; - -import javax.annotation.PreDestroy; - import feign.Client; import feign.httpclient.ApacheHttpClient; import org.apache.http.client.HttpClient; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.HttpClientConnectionManager; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory; -import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory; import org.springframework.cloud.netflix.ribbon.SpringClientFactory; -import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; +import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; /** * @author Spencer Gibb + * @author Olga Maciaszek-Sharma */ @Configuration @ConditionalOnClass(ApacheHttpClient.class) @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true) +@Import(HttpClientFeignConfiguration.class) class HttpClientFeignLoadBalancedConfiguration { @Bean @@ -57,84 +47,4 @@ class HttpClientFeignLoadBalancedConfiguration { return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory); } - @Configuration - @ConditionalOnMissingBean(CloseableHttpClient.class) - protected static class HttpClientFeignConfiguration { - - private final Timer connectionManagerTimer = new Timer( - "FeignApacheHttpClientConfiguration.connectionManagerTimer", true); - - private CloseableHttpClient httpClient; - - @Autowired(required = false) - private RegistryBuilder registryBuilder; - - @Bean - @ConditionalOnMissingBean(HttpClientConnectionManager.class) - public HttpClientConnectionManager connectionManager( - ApacheHttpClientConnectionManagerFactory connectionManagerFactory, - FeignHttpClientProperties httpClientProperties) { - final HttpClientConnectionManager connectionManager = connectionManagerFactory - .newConnectionManager(httpClientProperties.isDisableSslValidation(), - httpClientProperties.getMaxConnections(), - httpClientProperties.getMaxConnectionsPerRoute(), - httpClientProperties.getTimeToLive(), - httpClientProperties.getTimeToLiveUnit(), - this.registryBuilder); - this.connectionManagerTimer.schedule(new TimerTask() { - @Override - public void run() { - connectionManager.closeExpiredConnections(); - } - }, 30000, httpClientProperties.getConnectionTimerRepeat()); - return connectionManager; - } - - @Bean - @ConditionalOnProperty(value = "feign.compression.response.enabled", - havingValue = "true") - public CloseableHttpClient customHttpClient( - HttpClientConnectionManager httpClientConnectionManager, - FeignHttpClientProperties httpClientProperties) { - HttpClientBuilder builder = HttpClientBuilder.create() - .disableCookieManagement().useSystemProperties(); - this.httpClient = createClient(builder, httpClientConnectionManager, - httpClientProperties); - return this.httpClient; - } - - @Bean - @ConditionalOnProperty(value = "feign.compression.response.enabled", - havingValue = "false", matchIfMissing = true) - public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory, - HttpClientConnectionManager httpClientConnectionManager, - FeignHttpClientProperties httpClientProperties) { - this.httpClient = createClient(httpClientFactory.createBuilder(), - httpClientConnectionManager, httpClientProperties); - return this.httpClient; - } - - private CloseableHttpClient createClient(HttpClientBuilder builder, - HttpClientConnectionManager httpClientConnectionManager, - FeignHttpClientProperties httpClientProperties) { - RequestConfig defaultRequestConfig = RequestConfig.custom() - .setConnectTimeout(httpClientProperties.getConnectionTimeout()) - .setRedirectsEnabled(httpClientProperties.isFollowRedirects()) - .build(); - CloseableHttpClient httpClient = builder - .setDefaultRequestConfig(defaultRequestConfig) - .setConnectionManager(httpClientConnectionManager).build(); - return httpClient; - } - - @PreDestroy - public void destroy() throws Exception { - this.connectionManagerTimer.cancel(); - if (this.httpClient != null) { - this.httpClient.close(); - } - } - - } - } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/OkHttpFeignLoadBalancedConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/OkHttpFeignLoadBalancedConfiguration.java index 925a707f..7aae4aeb 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/OkHttpFeignLoadBalancedConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/OkHttpFeignLoadBalancedConfiguration.java @@ -16,30 +16,26 @@ package org.springframework.cloud.openfeign.ribbon; -import java.util.concurrent.TimeUnit; - -import javax.annotation.PreDestroy; - import feign.Client; import feign.okhttp.OkHttpClient; -import okhttp3.ConnectionPool; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; -import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; import org.springframework.cloud.netflix.ribbon.SpringClientFactory; -import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; +import org.springframework.cloud.openfeign.clientconfig.OkHttpFeignConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; /** * @author Spencer Gibb + * @author Olga Maciaszek-Sharma */ @Configuration @ConditionalOnClass(OkHttpClient.class) @ConditionalOnProperty("feign.okhttp.enabled") +@Import(OkHttpFeignConfiguration.class) class OkHttpFeignLoadBalancedConfiguration { @Bean @@ -50,45 +46,4 @@ class OkHttpFeignLoadBalancedConfiguration { return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory); } - @Configuration - @ConditionalOnMissingBean(okhttp3.OkHttpClient.class) - protected static class OkHttpFeignConfiguration { - - private okhttp3.OkHttpClient okHttpClient; - - @Bean - @ConditionalOnMissingBean(ConnectionPool.class) - public ConnectionPool httpClientConnectionPool( - FeignHttpClientProperties httpClientProperties, - OkHttpClientConnectionPoolFactory connectionPoolFactory) { - Integer maxTotalConnections = httpClientProperties.getMaxConnections(); - Long timeToLive = httpClientProperties.getTimeToLive(); - TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit(); - return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit); - } - - @Bean - public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, - ConnectionPool connectionPool, - FeignHttpClientProperties httpClientProperties) { - Boolean followRedirects = httpClientProperties.isFollowRedirects(); - Integer connectTimeout = httpClientProperties.getConnectionTimeout(); - this.okHttpClient = httpClientFactory - .createBuilder(httpClientProperties.isDisableSslValidation()) - .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) - .followRedirects(followRedirects).connectionPool(connectionPool) - .build(); - return this.okHttpClient; - } - - @PreDestroy - public void destroy() { - if (this.okHttpClient != null) { - this.okHttpClient.dispatcher().executorService().shutdown(); - this.okHttpClient.connectionPool().evictAll(); - } - } - - } - } diff --git a/spring-cloud-openfeign-core/src/main/resources/META-INF/spring.factories b/spring-cloud-openfeign-core/src/main/resources/META-INF/spring.factories index 27e7c30c..cd46e46c 100644 --- a/spring-cloud-openfeign-core/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-openfeign-core/src/main/resources/META-INF/spring.factories @@ -3,4 +3,5 @@ org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\ org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,\ org.springframework.cloud.openfeign.FeignAutoConfiguration,\ org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\ -org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration +org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,\ +org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClientTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClientTests.java new file mode 100644 index 00000000..e8502bb0 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClientTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2013-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.openfeign.loadbalancer; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import feign.Client; +import feign.Request; +import feign.Response; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link FeignBlockingLoadBalancerClient}. Note: the underlying + * {@link BlockingLoadBalancerClient} is already extensively tested in the Spring Cloud + * Commons project, so here we are only testing the interactions between + * {@link FeignBlockingLoadBalancerClient} and its delegates. + * + * @see BlockingLoadBalancerClientTests + * @author Olga Maciaszek-Sharma + */ +@ExtendWith(MockitoExtension.class) +class FeignBlockingLoadBalancerClientTests { + + private Client delegate = mock(Client.class); + + private BlockingLoadBalancerClient loadBalancerClient = mock( + BlockingLoadBalancerClient.class); + + private FeignBlockingLoadBalancerClient feignBlockingLoadBalancerClient = new FeignBlockingLoadBalancerClient( + delegate, loadBalancerClient); + + @Test + void shouldExtractServiceIdFromRequestUrl() throws IOException { + Request request = testRequest(); + + feignBlockingLoadBalancerClient.execute(request, new Request.Options()); + + verify(loadBalancerClient).choose("test"); + } + + @Test + void shouldThrowExceptionIfNoServiceId() { + Request request = testRequest(""); + + assertThatIllegalStateException() + .isThrownBy(() -> feignBlockingLoadBalancerClient.execute(request, + new Request.Options())) + .withMessage( + "Request URI does not contain a valid hostname: http:///path"); + } + + @Test + void shouldRespondWithServiceUnavailableIfInstanceNotFound() throws IOException { + Request request = testRequest(); + + Response response = feignBlockingLoadBalancerClient.execute(request, + new Request.Options()); + + assertThat(response.status()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE.value()); + assertThat(response.body().toString()).isEqualTo( + "Load balancer does not contain an instance for the service test"); + } + + @Test + void shouldPassCorrectRequestToDelegate() throws IOException { + Request request = testRequest(); + Request.Options options = new Request.Options(); + String url = "http://127.0.0.1/path"; + ServiceInstance serviceInstance = new DefaultServiceInstance("test-1", "test", + "test-host", 8888, false); + when(loadBalancerClient.choose("test")).thenReturn(serviceInstance); + when(loadBalancerClient.reconstructURI(serviceInstance, + URI.create("http://test/path"))).thenReturn(URI.create(url)); + + feignBlockingLoadBalancerClient.execute(request, options); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Request.class); + verify(delegate, times(1)).execute(captor.capture(), eq(options)); + Request actualRequest = captor.getValue(); + assertThat(actualRequest.httpMethod()).isEqualTo(Request.HttpMethod.GET); + assertThat(actualRequest.url()).isEqualTo(url); + assertThat(actualRequest.headers()).hasSize(1); + assertThat(actualRequest.headers()).containsEntry(HttpHeaders.CONTENT_TYPE, + Collections.singletonList(MediaType.APPLICATION_JSON_VALUE)); + assertThat(actualRequest.requestBody().asString()).isEqualTo("hello"); + } + + private Request testRequest() { + return testRequest("test"); + } + + private Request testRequest(String host) { + return Request.create(Request.HttpMethod.GET, "http://" + host + "/path", + testHeaders(), + Request.Body.encoded("hello".getBytes(), StandardCharsets.UTF_8)); + } + + private Map> testHeaders() { + Map> feignHeaders = new HashMap<>(); + feignHeaders.put(HttpHeaders.CONTENT_TYPE, + Collections.singletonList(MediaType.APPLICATION_JSON_VALUE)); + return feignHeaders; + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfigurationTests.java new file mode 100644 index 00000000..0a809853 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfigurationTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2013-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.openfeign.loadbalancer; + +import java.util.Map; + +import feign.Client; +import feign.httpclient.ApacheHttpClient; +import feign.okhttp.OkHttpClient; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.commons.httpclient.HttpClientConfiguration; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration; +import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration; +import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration; +import org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration; +import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Olga Maciaszek-Sharma + */ +class FeignLoadBalancerAutoConfigurationTests { + + @Test + void shouldInstantiateDefaultFeignBlockingLoadBalancerClientWhenHttpClientDisabled() { + ConfigurableApplicationContext context = initContext( + "spring.cloud.loadbalancer.ribbon.enabled=false", + "feign.httpclient.enabled=false"); + assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class); + assertLoadBalanced(context, Client.Default.class); + assertThatBeanNotPresent(context, LoadBalancerFeignClient.class); + } + + @Test + void shouldInstantiateHttpFeignClientWhenEnabled() { + ConfigurableApplicationContext context = initContext( + "spring.cloud.loadbalancer.ribbon.enabled=false"); + assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class); + assertLoadBalanced(context, ApacheHttpClient.class); + assertThatBeanNotPresent(context, LoadBalancerFeignClient.class); + } + + @Test + void shouldInstantiateOkHttpFeignClientWhenEnabled() { + ConfigurableApplicationContext context = initContext( + "spring.cloud.loadbalancer.ribbon.enabled=false", + "feign.httpclient.enabled=false", "feign.okhttp.enabled=true"); + assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class); + assertLoadBalanced(context, OkHttpClient.class); + assertThatBeanNotPresent(context, LoadBalancerFeignClient.class); + } + + @Test + void shouldNotProcessLoadBalancerConfigurationWhenRibbonEnabled() { + ConfigurableApplicationContext context = initContext( + "spring.cloud.loadbalancer.ribbon.enabled=true"); + assertThatOneBeanPresent(context, LoadBalancerFeignClient.class); + assertThatBeanNotPresent(context, BlockingLoadBalancerClient.class); + assertThatBeanNotPresent(context, FeignBlockingLoadBalancerClient.class); + } + + private ConfigurableApplicationContext initContext(String... properties) { + return new SpringApplicationBuilder().web(WebApplicationType.NONE) + .properties(properties) + .sources(HttpClientConfiguration.class, RibbonAutoConfiguration.class, + LoadBalancerAutoConfiguration.class, + BlockingLoadBalancerClientAutoConfiguration.class, + FeignRibbonClientAutoConfiguration.class, + FeignLoadBalancerAutoConfiguration.class) + .run(); + } + + private void assertThatOneBeanPresent(ConfigurableApplicationContext context, + Class beanClass) { + Map beans = context.getBeansOfType(beanClass); + assertThat(beans).hasSize(1); + } + + private void assertLoadBalanced(ConfigurableApplicationContext context, + Class delegateClass) { + Map beans = context + .getBeansOfType(FeignBlockingLoadBalancerClient.class); + assertThat(beans).hasSize(1); + assertThat(beans.get("feignClient").getDelegate()).isInstanceOf(delegateClass); + } + + private void assertThatBeanNotPresent(ConfigurableApplicationContext context, + Class beanClass) { + Map beans = context.getBeansOfType(beanClass); + assertThat(beans).isEmpty(); + } + +} diff --git a/spring-cloud-starter-openfeign/pom.xml b/spring-cloud-starter-openfeign/pom.xml index e9ad7d80..d86f6921 100644 --- a/spring-cloud-starter-openfeign/pom.xml +++ b/spring-cloud-starter-openfeign/pom.xml @@ -62,5 +62,10 @@ spring-cloud-starter-netflix-archaius true + + org.springframework.cloud + spring-cloud-starter-loadbalancer + true +