diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc
index 012ed963..efbb3227 100644
--- a/docs/src/main/asciidoc/_configprops.adoc
+++ b/docs/src/main/asciidoc/_configprops.adoc
@@ -18,6 +18,11 @@
|feign.httpclient.disable-ssl-validation | `false` |
|feign.httpclient.enabled | `true` | Enables the use of the Apache HTTP Client by Feign.
|feign.httpclient.follow-redirects | `true` |
+|feign.httpclient.hc5.enabled | `false` | Enables the use of the Apache HTTP Client 5 by Feign.
+|feign.httpclient.hc5.pool-concurrency-policy | | Pool concurrency policies.
+|feign.httpclient.hc5.pool-reuse-policy | | Pool connection re-use policies.
+|feign.httpclient.hc5.socket-timeout | `5` | Default value for socket timeout.
+|feign.httpclient.hc5.socket-timeout-unit | | Default value for socket timeout unit.
|feign.httpclient.max-connections | `200` |
|feign.httpclient.max-connections-per-route | `50` |
|feign.httpclient.time-to-live | `900` |
diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc
index 01d92864..120c1bb4 100644
--- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc
@@ -121,8 +121,8 @@ If none of them is on the classpath, the default feign client is used.
NOTE: `spring-cloud-starter-openfeign` supports `spring-cloud-starter-loadbalancer`. However, as is an optional dependency, you need to make sure it been added to your project if you want to use it.
-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 19a7d322..cb07ac20 100644
--- a/spring-cloud-openfeign-core/pom.xml
+++ b/spring-cloud-openfeign-core/pom.xml
@@ -109,6 +109,11 @@
feign-httpclient
true
+
+ io.github.openfeign
+ feign-hc5
+ true
+
io.github.openfeign
feign-okhttp
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 37a080cd..e155db4b 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
@@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.Module;
import feign.Client;
import feign.Feign;
import feign.RequestInterceptor;
+import feign.hc5.ApacheHttp5Client;
import feign.httpclient.ApacheHttpClient;
import feign.okhttp.OkHttpClient;
import okhttp3.ConnectionPool;
@@ -44,6 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.actuator.HasFeatures;
@@ -75,6 +77,7 @@ import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResour
* @author Nikita Konev
* @author Tim Peeters
* @author Olga Maciaszek-Sharma
+ * @author Nguyen Ky Thanh
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@@ -159,6 +162,7 @@ public class FeignAutoConfiguration {
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
+ @Conditional(HttpClient5DisabledConditions.class)
protected static class HttpClientFeignConfiguration {
private final Timer connectionManagerTimer = new Timer(
@@ -268,6 +272,21 @@ public class FeignAutoConfiguration {
}
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(ApacheHttp5Client.class)
+ @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);
+ }
+
+ }
+
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OAuth2ClientContext.class)
@ConditionalOnProperty("feign.oauth2.enabled")
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..bfb6112d
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/HttpClient5DisabledConditions.java
@@ -0,0 +1,42 @@
+/*
+ * 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..ffbbbbe0
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/HttpClient5FeignConfiguration.java
@@ -0,0 +1,146 @@
+/*
+ * 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 ec419002..5acfbd03 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;
* of {@link Client}.
*
* @author Olga Maciaszek-Sharma
+ * @author Nguyen Ky Thanh
* @since 2.2.0
*/
@ConditionalOnClass(Feign.class)
@@ -50,7 +51,7 @@ import org.springframework.context.annotation.Import;
// see
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
- DefaultFeignLoadBalancerConfiguration.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..9d3b2d9a
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/HttpClient5FeignLoadBalancerConfiguration.java
@@ -0,0 +1,76 @@
+/*
+ * 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 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.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
+import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+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;
+
+/**
+ * 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({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
+@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
+@Import(HttpClient5FeignConfiguration.class)
+@EnableConfigurationProperties(LoadBalancerProperties.class)
+class HttpClient5FeignLoadBalancerConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @Conditional(OnRetryNotEnabledCondition.class)
+ public Client feignClient(BlockingLoadBalancerClient loadBalancerClient, HttpClient httpClient5,
+ LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
+ Client delegate = new ApacheHttp5Client(httpClient5);
+ return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
+ }
+
+ @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,
+ LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
+ LoadBalancerClientFactory loadBalancerClientFactory) {
+ Client delegate = new ApacheHttp5Client(httpClient5);
+ return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
+ properties, loadBalancerClientFactory);
+ }
+
+}
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 91612f72..ce176d2b 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.
@@ -29,6 +29,7 @@ import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+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.context.annotation.Import;
* 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({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
+@Conditional(HttpClient5DisabledConditions.class)
@Import(HttpClientFeignConfiguration.class)
@EnableConfigurationProperties(LoadBalancerProperties.class)
class HttpClientFeignLoadBalancerConfiguration {
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..ad3b52e1 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,124 @@ 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 0f5b8c77..0ed20cd1 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
@@ -20,6 +20,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..b1a57c1a
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClient5ConfigurationTests.java
@@ -0,0 +1,143 @@
+/*
+ * 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 3c720848..43834169 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;
@@ -35,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Olga Maciaszek-Sharma
+ * @author Nguyen Ky Thanh
*/
class FeignLoadBalancerAutoConfigurationTests {
@@ -61,29 +63,62 @@ class FeignLoadBalancerAutoConfigurationTests {
assertLoadBalanced(context, OkHttpClient.class);
}
+ @Test
+ void shouldInstantiateHttpFeignClient5WhenEnabled() {
+ ConfigurableApplicationContext context = initContext("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);
+ }
+
+ @Test
+ void shouldInstantiateHttpFeignClient5WhenBothHttpClientAndHttpClient5Enabled() {
+ ConfigurableApplicationContext context = initContext("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);
+ }
+
@Test
void shouldInstantiateRetryableDefaultFeignBlockingLoadBalancerClientWhenHttpClientDisabled() {
- ConfigurableApplicationContext context = initContext("spring.cloud.loadbalancer.ribbon.enabled=false",
- "feign.httpclient.enabled=false");
+ ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=false");
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
assertLoadBalancedWithRetries(context, Client.Default.class);
}
@Test
void shouldInstantiateRetryableHttpFeignClientWhenEnabled() {
- ConfigurableApplicationContext context = initContext("spring.cloud.loadbalancer.ribbon.enabled=false");
+ ConfigurableApplicationContext context = initContext();
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
assertLoadBalancedWithRetries(context, ApacheHttpClient.class);
}
@Test
void shouldInstantiateRetryableOkHttpFeignClientWhenEnabled() {
- ConfigurableApplicationContext context = initContext("spring.cloud.loadbalancer.ribbon.enabled=false",
- "feign.httpclient.enabled=false", "feign.okhttp.enabled=true");
+ ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=false",
+ "feign.okhttp.enabled=true");
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
assertLoadBalancedWithRetries(context, OkHttpClient.class);
}
+ @Test
+ void shouldInstantiateRetryableHttpFeignClient5WhenEnabled() {
+ ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=false",
+ "feign.okhttp.enabled=false", "feign.httpclient.hc5.enabled=true");
+ assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
+ assertLoadBalancedWithRetries(context, ApacheHttp5Client.class);
+ }
+
+ @Test
+ void shouldInstantiateRetryableHttpFeignClient5WhenBothHttpClientAndHttpClient5Enabled() {
+ ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=true",
+ "feign.okhttp.enabled=false", "feign.httpclient.hc5.enabled=true");
+ assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
+ assertLoadBalancedWithRetries(context, ApacheHttp5Client.class);
+ }
+
private ConfigurableApplicationContext initContext(String... properties) {
return new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties)
.sources(HttpClientConfiguration.class, LoadBalancerAutoConfiguration.class,
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 f08048a2..0835b322 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
@@ -59,6 +66,10 @@ public class FeignHttpClientPropertiesTests {
assertThat(getProperties().isDisableSslValidation())
.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
@@ -66,7 +77,10 @@ public class FeignHttpClientPropertiesTests {
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.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);
@@ -75,6 +89,10 @@ 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() {