diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc
index 5c97606b..36dfc82e 100644
--- a/docs/src/main/asciidoc/_configprops.adoc
+++ b/docs/src/main/asciidoc/_configprops.adoc
@@ -24,5 +24,9 @@
|feign.httpclient.time-to-live-unit | |
|feign.hystrix.enabled | `false` | If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker.
|feign.okhttp.enabled | `false` | Enables the use of the OK HTTP Client by Feign.
-
-|===
\ No newline at end of file
+|feign.httpclient.hc5.enabled | `false` | Enables the use of the Apache HC5 by Feign.
+|feign.httpclient.hc5.socket-timeout | `5` |
+|feign.httpclient.hc5.socket-timeout-unit | `seconds` |
+|feign.httpclient.hc5.pool-reuse-policy | `fifo` | Pooled connection re-use policies
+|feign.httpclient.hc5.pool-concurrency-policy | `strict` | Pool concurrency policies
+|===
diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc
index 0a58243a..a8214d27 100644
--- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc
@@ -125,8 +125,8 @@ If none of them is in the classpath, the default feign client is used.
NOTE: `spring-cloud-starter-openfeign` supports both `spring-cloud-starter-netflix-ribbon` and `spring-cloud-starter-loadbalancer`. However, as they are optional dependencies, you need to make sure the one you want to use has been added to your project.
-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.
+The OkHttpClient and ApacheHttpClient and ApacheHC5 feign clients can be used by setting `feign.okhttp.enabled` or `feign.httpclient.enabled` or `feign.httpclient.hc5.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 or `org.apache.hc.client5.http.impl.classic.CloseableHttpClient` when using Apache HC5.
Spring Cloud OpenFeign _does not_ provide the following beans by default for feign, but still looks up beans of these types from the application context to create the feign client:
diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml
index ba151a25..02e3b850 100644
--- a/spring-cloud-openfeign-core/pom.xml
+++ b/spring-cloud-openfeign-core/pom.xml
@@ -108,6 +108,11 @@
feign-slf4j
true
+
+ io.github.openfeign
+ feign-hc5
+ true
+
io.github.openfeign
feign-httpclient
diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java
index 9c563c28..eff3077f 100644
--- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java
@@ -28,6 +28,7 @@ import javax.annotation.PreDestroy;
import com.fasterxml.jackson.databind.Module;
import feign.Client;
import feign.Feign;
+import feign.hc5.ApacheHttp5Client;
import feign.httpclient.ApacheHttpClient;
import feign.okhttp.OkHttpClient;
import okhttp3.ConnectionPool;
@@ -73,6 +74,7 @@ import org.springframework.data.domain.Sort;
* @author Nikita Konev
* @author Tim Peeters
* @author Olga Maciaszek-Sharma
+ * @author Nguyen Ky Thanh
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@@ -169,6 +171,7 @@ public class FeignAutoConfiguration {
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
+ @Conditional(HttpClient5DisabledConditions.class)
protected static class HttpClientFeignConfiguration {
private final Timer connectionManagerTimer = new Timer(
@@ -287,6 +290,23 @@ public class FeignAutoConfiguration {
}
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(ApacheHttp5Client.class)
+ @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
+ @ConditionalOnMissingBean(org.apache.hc.client5.http.impl.classic.CloseableHttpClient.class)
+ @ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
+ @Import(org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration.class)
+ protected static class HttpClient5FeignConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(Client.class)
+ public Client feignClient(
+ org.apache.hc.client5.http.impl.classic.CloseableHttpClient httpClient5) {
+ return new ApacheHttp5Client(httpClient5);
+ }
+
+ }
+
static class DefaultFeignTargeterConditions extends AllNestedConditions {
DefaultFeignTargeterConditions() {
diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/HttpClient5DisabledConditions.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/HttpClient5DisabledConditions.java
new file mode 100644
index 00000000..96b9dd76
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/HttpClient5DisabledConditions.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2013-2021 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;
+
+import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+/**
+ * @author Nguyen Ky Thanh
+ */
+public class HttpClient5DisabledConditions extends AnyNestedCondition {
+
+ public HttpClient5DisabledConditions() {
+ super(ConfigurationPhase.PARSE_CONFIGURATION);
+ }
+
+ @ConditionalOnMissingClass("feign.hc5.ApacheHttp5Client")
+ static class ApacheHttp5ClientClassMissing {
+
+ }
+
+ @ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "false",
+ matchIfMissing = true)
+ static class HttpClient5Disabled {
+
+ }
+
+}
diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/HttpClient5FeignConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/HttpClient5FeignConfiguration.java
new file mode 100644
index 00000000..7e1c8c4c
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/HttpClient5FeignConfiguration.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2013-2021 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.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.PreDestroy;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
+import org.apache.hc.core5.http.io.SocketConfig;
+import org.apache.hc.core5.http.ssl.TLS;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
+import org.apache.hc.core5.pool.PoolReusePolicy;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+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 Nguyen Ky Thanh
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnMissingBean(CloseableHttpClient.class)
+public class HttpClient5FeignConfiguration {
+
+ private static final Log LOG = LogFactory.getLog(HttpClient5FeignConfiguration.class);
+
+ private CloseableHttpClient httpClient5;
+
+ @Bean
+ @ConditionalOnMissingBean(HttpClientConnectionManager.class)
+ public HttpClientConnectionManager hc5ConnectionManager(
+ FeignHttpClientProperties httpClientProperties) {
+ return PoolingHttpClientConnectionManagerBuilder.create()
+ .setSSLSocketFactory(httpsSSLConnectionSocketFactory(
+ httpClientProperties.isDisableSslValidation()))
+ .setMaxConnTotal(httpClientProperties.getMaxConnections())
+ .setMaxConnPerRoute(httpClientProperties.getMaxConnectionsPerRoute())
+ .setConnPoolPolicy(PoolReusePolicy.valueOf(
+ httpClientProperties.getHc5().getPoolReusePolicy().name()))
+ .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.valueOf(
+ httpClientProperties.getHc5().getPoolConcurrencyPolicy().name()))
+ .setConnectionTimeToLive(
+ TimeValue.of(httpClientProperties.getTimeToLive(),
+ httpClientProperties.getTimeToLiveUnit()))
+ .setDefaultSocketConfig(SocketConfig.custom()
+ .setSoTimeout(Timeout.of(
+ httpClientProperties.getHc5().getSocketTimeout(),
+ httpClientProperties.getHc5().getSocketTimeoutUnit()))
+ .build())
+ .build();
+ }
+
+ @Bean
+ public CloseableHttpClient httpClient5(HttpClientConnectionManager connectionManager,
+ FeignHttpClientProperties httpClientProperties) {
+ httpClient5 = HttpClients.custom().disableCookieManagement().useSystemProperties()
+ .setConnectionManager(connectionManager).evictExpiredConnections()
+ .setDefaultRequestConfig(
+ RequestConfig.custom()
+ .setConnectTimeout(Timeout.of(
+ httpClientProperties.getConnectionTimeout(),
+ TimeUnit.MILLISECONDS))
+ .setRedirectsEnabled(
+ httpClientProperties.isFollowRedirects())
+ .build())
+ .build();
+ return httpClient5;
+ }
+
+ @PreDestroy
+ public void destroy() {
+ if (httpClient5 != null) {
+ httpClient5.close(CloseMode.GRACEFUL);
+ }
+ }
+
+ private LayeredConnectionSocketFactory httpsSSLConnectionSocketFactory(
+ boolean isDisableSslValidation) {
+ final SSLConnectionSocketFactoryBuilder sslConnectionSocketFactoryBuilder = SSLConnectionSocketFactoryBuilder
+ .create().setTlsVersions(TLS.V_1_3, TLS.V_1_2);
+
+ if (isDisableSslValidation) {
+ try {
+ final SSLContext sslContext = SSLContext.getInstance("SSL");
+ sslContext.init(null,
+ new TrustManager[] { new DisabledValidationTrustManager() },
+ new SecureRandom());
+ sslConnectionSocketFactoryBuilder.setSslContext(sslContext);
+ }
+ catch (NoSuchAlgorithmException e) {
+ LOG.warn("Error creating SSLContext", e);
+ }
+ catch (KeyManagementException e) {
+ LOG.warn("Error creating SSLContext", e);
+ }
+ }
+ else {
+ sslConnectionSocketFactoryBuilder
+ .setSslContext(SSLContexts.createSystemDefault());
+ }
+
+ return sslConnectionSocketFactoryBuilder.build();
+ }
+
+ static class DisabledValidationTrustManager implements X509TrustManager {
+
+ DisabledValidationTrustManager() {
+ }
+
+ public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
+ throws CertificateException {
+ }
+
+ public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
+ throws CertificateException {
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+ }
+
+}
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
index 9f72c86f..69f96a3f 100644
--- 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2021 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.
@@ -38,6 +38,7 @@ import org.springframework.context.annotation.Import;
* spring.cloud.loadbalancer.ribbon.enabled to true.
*
* @author Olga Maciaszek-Sharma
+ * @author Nguyen Ky Thanh
* @since 2.2.0
*/
@ConditionalOnClass(Feign.class)
@@ -51,6 +52,7 @@ import org.springframework.context.annotation.Import;
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ HttpClientFeignLoadBalancerConfiguration.class,
OkHttpFeignLoadBalancerConfiguration.class,
+ HttpClient5FeignLoadBalancerConfiguration.class,
DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {
diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClient5FeignLoadBalancerConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClient5FeignLoadBalancerConfiguration.java
new file mode 100644
index 00000000..beeb12f3
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClient5FeignLoadBalancerConfiguration.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2013-2021 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.List;
+
+import feign.Client;
+import feign.hc5.ApacheHttp5Client;
+import org.apache.hc.client5.http.classic.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.client.loadbalancer.LoadBalancedRetryFactory;
+import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
+import org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
+
+/**
+ * Configuration instantiating a {@link BlockingLoadBalancerClient}-based {@link Client}
+ * object that uses {@link ApacheHttp5Client} under the hood.
+ *
+ * @author Nguyen Ky Thanh
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass(ApacheHttp5Client.class)
+@ConditionalOnBean(BlockingLoadBalancerClient.class)
+@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
+@Import(HttpClient5FeignConfiguration.class)
+class HttpClient5FeignLoadBalancerConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @Conditional(OnRetryNotEnabledCondition.class)
+ public Client feignClient(BlockingLoadBalancerClient loadBalancerClient,
+ HttpClient httpClient5) {
+ Client delegate = new ApacheHttp5Client(httpClient5);
+ return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
+ @ConditionalOnBean(LoadBalancedRetryFactory.class)
+ @ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled",
+ havingValue = "true", matchIfMissing = true)
+ public Client feignRetryClient(BlockingLoadBalancerClient loadBalancerClient,
+ HttpClient httpClient5,
+ List loadBalancedRetryFactories) {
+ AnnotationAwareOrderComparator.sort(loadBalancedRetryFactories);
+ Client delegate = new ApacheHttp5Client(httpClient5);
+ return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient,
+ loadBalancedRetryFactories.get(0));
+ }
+
+}
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
index 569bbbd9..c5a6fc26 100644
--- 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2021 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.
@@ -28,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
+import org.springframework.cloud.openfeign.HttpClient5DisabledConditions;
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
@@ -40,12 +41,14 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator;
* object that uses {@link ApacheHttpClient} under the hood.
*
* @author Olga Maciaszek-Sharma
+ * @author Nguyen Ky Thanh
* @since 2.2.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnBean(BlockingLoadBalancerClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
+@Conditional(HttpClient5DisabledConditions.class)
@Import(HttpClientFeignConfiguration.class)
class HttpClientFeignLoadBalancerConfiguration {
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 a5f40cdb..ce6c90c7 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2021 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.
@@ -41,6 +41,7 @@ import org.springframework.context.annotation.Primary;
*
* @author Dave Syer
* @author Olga Maciaszek-Sharma
+ * @author Nguyen Ky Thanh
*/
@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled",
@@ -53,6 +54,7 @@ import org.springframework.context.annotation.Primary;
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
OkHttpFeignLoadBalancedConfiguration.class,
+ HttpClient5FeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {
diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/HttpClient5FeignLoadBalancedConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/HttpClient5FeignLoadBalancedConfiguration.java
new file mode 100644
index 00000000..8f542bc0
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/HttpClient5FeignLoadBalancedConfiguration.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013-2021 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.ribbon;
+
+import feign.Client;
+import feign.hc5.ApacheHttp5Client;
+import org.apache.hc.client5.http.classic.HttpClient;
+
+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.netflix.ribbon.SpringClientFactory;
+import org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * Configuration instantiating a {@link LoadBalancerFeignClient}-based {@link Client}
+ * object that uses {@link ApacheHttp5Client} under the hood.
+ *
+ * @author Nguyen Ky Thanh
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass(ApacheHttp5Client.class)
+@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
+@Import(HttpClient5FeignConfiguration.class)
+class HttpClient5FeignLoadBalancedConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(Client.class)
+ public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
+ SpringClientFactory clientFactory, HttpClient httpClient5) {
+ Client delegate = new ApacheHttp5Client(httpClient5);
+ return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
+ }
+
+}
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 dfcecb66..cd675b69 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2021 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.
@@ -24,18 +24,22 @@ 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.netflix.ribbon.SpringClientFactory;
+import org.springframework.cloud.openfeign.HttpClient5DisabledConditions;
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
+ * @author Nguyen Ky Thanh
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
+@Conditional(HttpClient5DisabledConditions.class)
@Import(HttpClientFeignConfiguration.class)
class HttpClientFeignLoadBalancedConfiguration {
diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/FeignHttpClientProperties.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/FeignHttpClientProperties.java
index f0e029b8..602caa4a 100644
--- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/FeignHttpClientProperties.java
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/FeignHttpClientProperties.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2021 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.
@@ -22,6 +22,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Ryan Baxter
+ * @author Nguyen Ky Thanh
*/
@ConfigurationProperties(prefix = "feign.httpclient")
public class FeignHttpClientProperties {
@@ -82,6 +83,11 @@ public class FeignHttpClientProperties {
private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;
+ /**
+ * Apache HttpClient5 additional properties.
+ */
+ private Hc5Properties hc5 = new Hc5Properties();
+
public int getConnectionTimerRepeat() {
return this.connectionTimerRepeat;
}
@@ -146,4 +152,125 @@ public class FeignHttpClientProperties {
this.connectionTimeout = connectionTimeout;
}
+ public Hc5Properties getHc5() {
+ return hc5;
+ }
+
+ public void setHc5(Hc5Properties hc5) {
+ this.hc5 = hc5;
+ }
+
+ public static class Hc5Properties {
+
+ /**
+ * Default value for pool concurrency policy.
+ */
+ public static final PoolConcurrencyPolicy DEFAULT_POOL_CONCURRENCY_POLICY = PoolConcurrencyPolicy.STRICT;
+
+ /**
+ * Default value for pool reuse policy.
+ */
+ public static final PoolReusePolicy DEFAULT_POOL_REUSE_POLICY = PoolReusePolicy.FIFO;
+
+ /**
+ * Default value for socket timeout.
+ */
+ public static final int DEFAULT_SOCKET_TIMEOUT = 5;
+
+ /**
+ * Default value for socket timeout unit.
+ */
+ public static final TimeUnit DEFAULT_SOCKET_TIMEOUT_UNIT = TimeUnit.SECONDS;
+
+ /**
+ * Pool concurrency policies.
+ */
+ private PoolConcurrencyPolicy poolConcurrencyPolicy = DEFAULT_POOL_CONCURRENCY_POLICY;
+
+ /**
+ * Pool connection re-use policies.
+ */
+ private PoolReusePolicy poolReusePolicy = DEFAULT_POOL_REUSE_POLICY;
+
+ /**
+ * Default value for socket timeout.
+ */
+ private int socketTimeout = DEFAULT_SOCKET_TIMEOUT;
+
+ /**
+ * Default value for socket timeout unit.
+ */
+ private TimeUnit socketTimeoutUnit = DEFAULT_SOCKET_TIMEOUT_UNIT;
+
+ public PoolConcurrencyPolicy getPoolConcurrencyPolicy() {
+ return this.poolConcurrencyPolicy;
+ }
+
+ public void setPoolConcurrencyPolicy(
+ PoolConcurrencyPolicy poolConcurrencyPolicy) {
+ this.poolConcurrencyPolicy = poolConcurrencyPolicy;
+ }
+
+ public PoolReusePolicy getPoolReusePolicy() {
+ return poolReusePolicy;
+ }
+
+ public void setPoolReusePolicy(PoolReusePolicy poolReusePolicy) {
+ this.poolReusePolicy = poolReusePolicy;
+ }
+
+ public TimeUnit getSocketTimeoutUnit() {
+ return socketTimeoutUnit;
+ }
+
+ public void setSocketTimeoutUnit(TimeUnit socketTimeoutUnit) {
+ this.socketTimeoutUnit = socketTimeoutUnit;
+ }
+
+ public int getSocketTimeout() {
+ return socketTimeout;
+ }
+
+ public void setSocketTimeout(int socketTimeout) {
+ this.socketTimeout = socketTimeout;
+ }
+
+ /**
+ * Enumeration of pool concurrency policies.
+ */
+ public enum PoolConcurrencyPolicy {
+
+ /**
+ * Higher concurrency but with lax connection max limit guarantees.
+ */
+ LAX,
+
+ /**
+ * Strict connection max limit guarantees.
+ */
+ STRICT
+
+ }
+
+ /**
+ * Enumeration of pooled connection re-use policies.
+ */
+ public enum PoolReusePolicy {
+
+ /**
+ * Re-use as few connections as possible making it possible for connections to
+ * become idle and expire.
+ */
+ LIFO,
+
+ /**
+ * Re-use all connections equally preventing them from becoming idle and
+ * expiring.
+ */
+ FIFO
+
+ }
+
+ }
+
}
diff --git a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index e9490b71..7ceb6008 100644
--- a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -26,6 +26,12 @@
"description": "Enables the use of the Apache HTTP Client by Feign.",
"defaultValue": "true"
},
+ {
+ "name": "feign.httpclient.hc5.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Enables the use of the Apache HTTP Client 5 by Feign.",
+ "defaultValue": "false"
+ },
{
"name": "feign.okhttp.enabled",
"type": "java.lang.Boolean",
diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClient5ConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClient5ConfigurationTests.java
new file mode 100644
index 00000000..77c2a79f
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClient5ConfigurationTests.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2013-2021 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;
+
+import feign.Client;
+import feign.hc5.ApacheHttp5Client;
+import feign.httpclient.ApacheHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.cloud.commons.httpclient.HttpClientConfiguration;
+import org.springframework.cloud.test.ClassPathExclusions;
+import org.springframework.cloud.test.ModifiedClassPathRunner;
+import org.springframework.context.ConfigurableApplicationContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Nguyen Ky Thanh
+ */
+public class FeignHttpClient5ConfigurationTests {
+
+ private static void verifyHc4BeansAvailable(ConfigurableApplicationContext context) {
+ org.apache.http.impl.client.CloseableHttpClient httpClient4 = context
+ .getBean(org.apache.http.impl.client.CloseableHttpClient.class);
+ assertThat(httpClient4).isNotNull();
+ org.apache.http.conn.HttpClientConnectionManager connectionManager4 = context
+ .getBean(org.apache.http.conn.HttpClientConnectionManager.class);
+ assertThat(connectionManager4).isInstanceOf(
+ org.apache.http.impl.conn.PoolingHttpClientConnectionManager.class);
+ Client client = context.getBean(Client.class);
+ assertThat(client).isInstanceOf(ApacheHttpClient.class);
+ }
+
+ private static void verifyHc5BeansAvailable(ConfigurableApplicationContext context) {
+ CloseableHttpClient httpClient = context.getBean(CloseableHttpClient.class);
+ assertThat(httpClient).isNotNull();
+ HttpClientConnectionManager connectionManager = context
+ .getBean(HttpClientConnectionManager.class);
+ assertThat(connectionManager)
+ .isInstanceOf(PoolingHttpClientConnectionManager.class);
+ Client client = context.getBean(Client.class);
+ assertThat(client).isInstanceOf(ApacheHttp5Client.class);
+ }
+
+ @RunWith(ModifiedClassPathRunner.class)
+ @ClassPathExclusions("ribbon-loadbalancer-{version:\\d.*}.jar")
+ public static class WithoutLoadBalancerInClasspath {
+
+ @Test
+ public void verifyHttpClient5AutoConfig() {
+ ConfigurableApplicationContext context = new SpringApplicationBuilder()
+ .properties("feign.httpclient.hc5.enabled=true",
+ "feign.httpclient.enabled=false")
+ .web(WebApplicationType.NONE)
+ .sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
+ .run();
+
+ verifyHc5BeansAvailable(context);
+
+ if (context != null) {
+ context.close();
+ }
+ }
+
+ @Test
+ public void hc5ShouldWinIfTheBothVersionsAvailable() {
+ ConfigurableApplicationContext context = new SpringApplicationBuilder()
+ .properties("feign.httpclient.hc5.enabled=true",
+ "feign.httpclient.enabled=true")
+ .web(WebApplicationType.NONE)
+ .sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
+ .run();
+
+ Client client = context.getBean(Client.class);
+ assertThat(client).isInstanceOf(ApacheHttp5Client.class);
+
+ if (context != null) {
+ context.close();
+ }
+ }
+
+ @Test
+ public void hc4ShouldBeTheDefaultIfHc5NotEnabled() {
+ ConfigurableApplicationContext context = new SpringApplicationBuilder()
+ .properties("feign.httpclient.hc5.enabled=false",
+ "feign.httpclient.enabled=true")
+ .web(WebApplicationType.NONE)
+ .sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
+ .run();
+
+ verifyHc4BeansAvailable(context);
+
+ if (context != null) {
+ context.close();
+ }
+ }
+
+ }
+
+ @RunWith(ModifiedClassPathRunner.class)
+ @ClassPathExclusions({ "ribbon-loadbalancer-{version:\\d.*}.jar",
+ "feign-hc5-{version:\\d.*}.jar", "httpclient5-{version:\\d.*}.jar",
+ "httpcore5-{version:\\d.*}.jar", "httpcore5-h2-{version:\\d.*}.jar" })
+ public static class WithoutLoadBalancerAndHc5InClasspath {
+
+ @Test
+ public void hc4ShouldWinEvenHc5ConfigEnabled() {
+ ConfigurableApplicationContext context = new SpringApplicationBuilder()
+ .properties("feign.httpclient.hc5.enabled=true")
+ .web(WebApplicationType.NONE)
+ .sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
+ .run();
+
+ verifyHc4BeansAvailable(context);
+
+ if (context != null) {
+ context.close();
+ }
+ }
+
+ @Test
+ public void hc4ShouldBeTheDefault() {
+ ConfigurableApplicationContext context = new SpringApplicationBuilder()
+ .web(WebApplicationType.NONE)
+ .sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
+ .run();
+
+ verifyHc4BeansAvailable(context);
+
+ if (context != null) {
+ context.close();
+ }
+ }
+
+ }
+
+}
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
index 72b29925..6a68c68f 100644
--- 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2021 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.
@@ -19,6 +19,7 @@ package org.springframework.cloud.openfeign.loadbalancer;
import java.util.Map;
import feign.Client;
+import feign.hc5.ApacheHttp5Client;
import feign.httpclient.ApacheHttpClient;
import feign.okhttp.OkHttpClient;
import org.junit.jupiter.api.Test;
@@ -38,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Olga Maciaszek-Sharma
+ * @author Nguyen Ky Thanh
*/
class FeignLoadBalancerAutoConfigurationTests {
@@ -73,6 +75,30 @@ class FeignLoadBalancerAutoConfigurationTests {
assertThatBeanNotPresent(context, LoadBalancerFeignClient.class);
}
+ @Test
+ void shouldInstantiateHttpFeignClient5WhenEnabled() {
+ ConfigurableApplicationContext context = initContext(
+ "spring.cloud.loadbalancer.ribbon.enabled=false",
+ "feign.httpclient.enabled=false", "feign.okhttp.enabled=false",
+ "feign.httpclient.hc5.enabled=true",
+ "spring.cloud.loadbalancer.retry.enabled=false");
+ assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
+ assertLoadBalanced(context, ApacheHttp5Client.class);
+ assertThatBeanNotPresent(context, LoadBalancerFeignClient.class);
+ }
+
+ @Test
+ void shouldInstantiateHttpFeignClient5WhenBothHttpClientAndHttpClient5Enabled() {
+ ConfigurableApplicationContext context = initContext(
+ "spring.cloud.loadbalancer.ribbon.enabled=false",
+ "feign.httpclient.enabled=true", "feign.okhttp.enabled=false",
+ "feign.httpclient.hc5.enabled=true",
+ "spring.cloud.loadbalancer.retry.enabled=false");
+ assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
+ assertLoadBalanced(context, ApacheHttp5Client.class);
+ assertThatBeanNotPresent(context, LoadBalancerFeignClient.class);
+ }
+
@Test
void shouldInstantiateRetryableDefaultFeignBlockingLoadBalancerClientWhenHttpClientDisabled() {
ConfigurableApplicationContext context = initContext(
@@ -102,6 +128,28 @@ class FeignLoadBalancerAutoConfigurationTests {
assertThatBeanNotPresent(context, LoadBalancerFeignClient.class);
}
+ @Test
+ void shouldInstantiateRetryableHttpFeignClient5WhenEnabled() {
+ ConfigurableApplicationContext context = initContext(
+ "spring.cloud.loadbalancer.ribbon.enabled=false",
+ "feign.httpclient.enabled=false", "feign.okhttp.enabled=false",
+ "feign.httpclient.hc5.enabled=true");
+ assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
+ assertLoadBalancedWithRetries(context, ApacheHttp5Client.class);
+ assertThatBeanNotPresent(context, LoadBalancerFeignClient.class);
+ }
+
+ @Test
+ void shouldInstantiateRetryableHttpFeignClient5WhenBothHttpClientAndHttpClient5Enabled() {
+ ConfigurableApplicationContext context = initContext(
+ "spring.cloud.loadbalancer.ribbon.enabled=false",
+ "feign.httpclient.enabled=true", "feign.okhttp.enabled=false",
+ "feign.httpclient.hc5.enabled=true");
+ assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
+ assertLoadBalancedWithRetries(context, ApacheHttp5Client.class);
+ assertThatBeanNotPresent(context, LoadBalancerFeignClient.class);
+ }
+
@Test
void shouldNotProcessLoadBalancerConfigurationWhenRibbonEnabled() {
ConfigurableApplicationContext context = initContext(
diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientPathTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientPathTests.java
index 5749423c..b89f02fa 100644
--- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientPathTests.java
+++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientPathTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2021 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.
@@ -50,9 +50,8 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
webEnvironment = RANDOM_PORT,
value = { "spring.application.name=feignribbonclientpathtest",
"feign.okhttp.enabled=false", "feign.httpclient.enabled=false",
- "feign.hystrix.enabled=false", "test.path.prefix=/base/path" // For
- // pathWithPlaceholder
- // test
+ "feign.hystrix.enabled=false", "feign.httpclient.hc5.enabled=false",
+ "test.path.prefix=/base/path" // For pathWithPlaceholder test
})
@DirtiesContext
public class FeignRibbonClientPathTests {
diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientRetryTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientRetryTests.java
index 8782b704..0e09ca17 100644
--- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientRetryTests.java
+++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonClientRetryTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2021 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.
@@ -56,7 +56,8 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
webEnvironment = RANDOM_PORT,
value = { "spring.application.name=feignclientretrytest",
"feign.okhttp.enabled=false", "feign.httpclient.enabled=false",
- "feign.hystrix.enabled=false", "localapp.ribbon.MaxAutoRetries=2",
+ "feign.hystrix.enabled=false", "feign.httpclient.hc5.enabled=false",
+ "localapp.ribbon.MaxAutoRetries=2",
"localapp.ribbon.MaxAutoRetriesNextServer=3" })
@DirtiesContext
public class FeignRibbonClientRetryTests {
diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonHttpClient5ConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonHttpClient5ConfigurationTests.java
new file mode 100644
index 00000000..6a054919
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/ribbon/FeignRibbonHttpClient5ConfigurationTests.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2013-2021 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.ribbon;
+
+import java.lang.reflect.Field;
+
+import javax.net.ssl.SSLContextSpi;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+
+import feign.Client;
+import feign.hc5.ApacheHttp5Client;
+import org.apache.hc.client5.http.impl.io.DefaultHttpClientConnectionOperator;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.Lookup;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.util.ReflectionUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Nguyen Ky Thanh
+ */
+@SpringBootTest(
+ classes = FeignRibbonHttpClient5ConfigurationTests.FeignRibbonHttpClientConfigurationTestsApplication.class,
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+ properties = { "feign.httpclient.disableSslValidation=true",
+ "feign.httpclient.hc5.enabled=true", "feign.httpclient.enabled=false" })
+@DirtiesContext
+class FeignRibbonHttpClient5ConfigurationTests {
+
+ @Autowired
+ private HttpClientConnectionManager connectionManager;
+
+ @Autowired
+ private Client client;
+
+ @Test
+ void disableSslTest() throws Exception {
+ Lookup socketFactoryRegistry = getConnectionSocketFactoryLookup(
+ connectionManager);
+ assertThat(socketFactoryRegistry.lookup(URIScheme.HTTPS.id)).isNotNull();
+ assertThat(getX509TrustManager(socketFactoryRegistry).getAcceptedIssuers())
+ .isNull();
+ }
+
+ @Test
+ void verifyHttpClient5IsPickedUp() {
+ assertThat(client).isInstanceOf(LoadBalancerFeignClient.class);
+ Client delegate = (Client) getField(client, "delegate");
+ assertThat(delegate).isInstanceOf(ApacheHttp5Client.class);
+ }
+
+ private Lookup getConnectionSocketFactoryLookup(
+ HttpClientConnectionManager connectionManager) {
+ DefaultHttpClientConnectionOperator connectionOperator = (DefaultHttpClientConnectionOperator) this
+ .getField(connectionManager, "connectionOperator");
+ return (Lookup) getField(connectionOperator, "socketFactoryRegistry");
+ }
+
+ private X509TrustManager getX509TrustManager(
+ Lookup socketFactoryRegistry) {
+ ConnectionSocketFactory connectionSocketFactory = (ConnectionSocketFactory) socketFactoryRegistry
+ .lookup(URIScheme.HTTPS.id);
+ SSLSocketFactory sslSocketFactory = (SSLSocketFactory) this
+ .getField(connectionSocketFactory, "socketFactory");
+ SSLContextSpi sslContext = (SSLContextSpi) getField(sslSocketFactory, "context");
+ return (X509TrustManager) getField(sslContext, "trustManager");
+ }
+
+ protected Object getField(Object target, String name) {
+ Field field = ReflectionUtils.findField(target.getClass(), name);
+ ReflectionUtils.makeAccessible(field);
+ Object value = ReflectionUtils.getField(field, target);
+ return value;
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @EnableAutoConfiguration
+ static class FeignRibbonHttpClientConfigurationTestsApplication {
+
+ public static void main(String[] args) {
+ new SpringApplicationBuilder(FeignRibbonClientRetryTests.Application.class)
+ .run(args);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/FeignHttpClientPropertiesTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/FeignHttpClientPropertiesTests.java
index 0f46ec32..d6fca919 100644
--- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/FeignHttpClientPropertiesTests.java
+++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/FeignHttpClientPropertiesTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2020 the original author or authors.
+ * Copyright 2013-2021 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.
@@ -16,6 +16,8 @@
package org.springframework.cloud.openfeign.support;
+import java.util.concurrent.TimeUnit;
+
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -23,6 +25,8 @@ import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.util.TestPropertyValues;
+import org.springframework.cloud.openfeign.support.FeignHttpClientProperties.Hc5Properties.PoolConcurrencyPolicy;
+import org.springframework.cloud.openfeign.support.FeignHttpClientProperties.Hc5Properties.PoolReusePolicy;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -30,9 +34,12 @@ import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.cloud.openfeign.support.FeignHttpClientProperties.Hc5Properties.DEFAULT_SOCKET_TIMEOUT;
+import static org.springframework.cloud.openfeign.support.FeignHttpClientProperties.Hc5Properties.DEFAULT_SOCKET_TIMEOUT_UNIT;
/**
* @author Ryan Baxter
+ * @author Nguyen Ky Thanh
*/
@RunWith(SpringRunner.class)
@DirtiesContext
@@ -62,16 +69,32 @@ public class FeignHttpClientPropertiesTests {
.isEqualTo(FeignHttpClientProperties.DEFAULT_DISABLE_SSL_VALIDATION);
assertThat(getProperties().isFollowRedirects())
.isEqualTo(FeignHttpClientProperties.DEFAULT_FOLLOW_REDIRECTS);
+ assertThat(getProperties().getHc5().getPoolConcurrencyPolicy())
+ .isEqualTo(PoolConcurrencyPolicy.STRICT);
+ assertThat(getProperties().getHc5().getPoolReusePolicy())
+ .isEqualTo(PoolReusePolicy.FIFO);
+ assertThat(getProperties().getHc5().getSocketTimeout())
+ .isEqualTo(DEFAULT_SOCKET_TIMEOUT);
+ assertThat(getProperties().getHc5().getSocketTimeoutUnit())
+ .isEqualTo(DEFAULT_SOCKET_TIMEOUT_UNIT);
}
@Test
public void testCustomization() {
- TestPropertyValues.of("feign.httpclient.maxConnections=2",
- "feign.httpclient.connectionTimeout=2",
- "feign.httpclient.maxConnectionsPerRoute=2",
- "feign.httpclient.timeToLive=2",
- "feign.httpclient.disableSslValidation=true",
- "feign.httpclient.followRedirects=false").applyTo(this.context);
+ TestPropertyValues
+ .of("feign.httpclient.maxConnections=2",
+ "feign.httpclient.connectionTimeout=2",
+ "feign.httpclient.maxConnectionsPerRoute=2",
+ "feign.httpclient.timeToLive=2",
+ "feign.httpclient.disableSslValidation=true",
+ "feign.httpclient.followRedirects=false",
+ "feign.httpclient.disableSslValidation=true",
+ "feign.httpclient.followRedirects=false",
+ "feign.httpclient.hc5.poolConcurrencyPolicy=lax",
+ "feign.httpclient.hc5.poolReusePolicy=lifo",
+ "feign.httpclient.hc5.socketTimeout=200",
+ "feign.httpclient.hc5.socketTimeoutUnit=milliseconds")
+ .applyTo(this.context);
setupContext();
assertThat(getProperties().getMaxConnections()).isEqualTo(2);
assertThat(getProperties().getConnectionTimeout()).isEqualTo(2);
@@ -79,6 +102,13 @@ public class FeignHttpClientPropertiesTests {
assertThat(getProperties().getTimeToLive()).isEqualTo(2L);
assertThat(getProperties().isDisableSslValidation()).isTrue();
assertThat(getProperties().isFollowRedirects()).isFalse();
+ assertThat(getProperties().getHc5().getPoolConcurrencyPolicy())
+ .isEqualTo(PoolConcurrencyPolicy.LAX);
+ assertThat(getProperties().getHc5().getPoolReusePolicy())
+ .isEqualTo(PoolReusePolicy.LIFO);
+ assertThat(getProperties().getHc5().getSocketTimeout()).isEqualTo(200);
+ assertThat(getProperties().getHc5().getSocketTimeoutUnit())
+ .isEqualTo(TimeUnit.MILLISECONDS);
}
private void setupContext() {