Skip to content

Commit e3439f3

Browse files
authored
Add Apache HC5 client option (#498)
1 parent 2a23b6d commit e3439f3

20 files changed

+875
-23
lines changed

docs/src/main/asciidoc/_configprops.adoc

+6-2
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,9 @@
2424
|feign.httpclient.time-to-live-unit | |
2525
|feign.hystrix.enabled | `false` | If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker.
2626
|feign.okhttp.enabled | `false` | Enables the use of the OK HTTP Client by Feign.
27-
28-
|===
27+
|feign.httpclient.hc5.enabled | `false` | Enables the use of the Apache HC5 by Feign.
28+
|feign.httpclient.hc5.socket-timeout | `5` |
29+
|feign.httpclient.hc5.socket-timeout-unit | `seconds` |
30+
|feign.httpclient.hc5.pool-reuse-policy | `fifo` | Pooled connection re-use policies
31+
|feign.httpclient.hc5.pool-concurrency-policy | `strict` | Pool concurrency policies
32+
|===

docs/src/main/asciidoc/spring-cloud-openfeign.adoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ If none of them is in the classpath, the default feign client is used.
125125

126126
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.
127127

128-
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.
129-
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.
128+
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.
129+
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.
130130

131131
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:
132132

spring-cloud-openfeign-core/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@
108108
<artifactId>feign-slf4j</artifactId>
109109
<optional>true</optional>
110110
</dependency>
111+
<dependency>
112+
<groupId>io.github.openfeign</groupId>
113+
<artifactId>feign-hc5</artifactId>
114+
<optional>true</optional>
115+
</dependency>
111116
<dependency>
112117
<groupId>io.github.openfeign</groupId>
113118
<artifactId>feign-httpclient</artifactId>

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java

+20
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.fasterxml.jackson.databind.Module;
2929
import feign.Client;
3030
import feign.Feign;
31+
import feign.hc5.ApacheHttp5Client;
3132
import feign.httpclient.ApacheHttpClient;
3233
import feign.okhttp.OkHttpClient;
3334
import okhttp3.ConnectionPool;
@@ -73,6 +74,7 @@
7374
* @author Nikita Konev
7475
* @author Tim Peeters
7576
* @author Olga Maciaszek-Sharma
77+
* @author Nguyen Ky Thanh
7678
*/
7779
@Configuration(proxyBeanMethods = false)
7880
@ConditionalOnClass(Feign.class)
@@ -169,6 +171,7 @@ public Targeter circuitBreakerFeignTargeter(
169171
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
170172
@ConditionalOnMissingBean(CloseableHttpClient.class)
171173
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
174+
@Conditional(HttpClient5DisabledConditions.class)
172175
protected static class HttpClientFeignConfiguration {
173176

174177
private final Timer connectionManagerTimer = new Timer(
@@ -287,6 +290,23 @@ public Client feignClient(okhttp3.OkHttpClient client) {
287290

288291
}
289292

293+
@Configuration(proxyBeanMethods = false)
294+
@ConditionalOnClass(ApacheHttp5Client.class)
295+
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
296+
@ConditionalOnMissingBean(org.apache.hc.client5.http.impl.classic.CloseableHttpClient.class)
297+
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
298+
@Import(org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration.class)
299+
protected static class HttpClient5FeignConfiguration {
300+
301+
@Bean
302+
@ConditionalOnMissingBean(Client.class)
303+
public Client feignClient(
304+
org.apache.hc.client5.http.impl.classic.CloseableHttpClient httpClient5) {
305+
return new ApacheHttp5Client(httpClient5);
306+
}
307+
308+
}
309+
290310
static class DefaultFeignTargeterConditions extends AllNestedConditions {
291311

292312
DefaultFeignTargeterConditions() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2013-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.openfeign;
18+
19+
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
20+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
21+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
22+
23+
/**
24+
* @author Nguyen Ky Thanh
25+
*/
26+
public class HttpClient5DisabledConditions extends AnyNestedCondition {
27+
28+
public HttpClient5DisabledConditions() {
29+
super(ConfigurationPhase.PARSE_CONFIGURATION);
30+
}
31+
32+
@ConditionalOnMissingClass("feign.hc5.ApacheHttp5Client")
33+
static class ApacheHttp5ClientClassMissing {
34+
35+
}
36+
37+
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "false",
38+
matchIfMissing = true)
39+
static class HttpClient5Disabled {
40+
41+
}
42+
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2013-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.openfeign.clientconfig;
18+
19+
import java.security.KeyManagementException;
20+
import java.security.NoSuchAlgorithmException;
21+
import java.security.SecureRandom;
22+
import java.security.cert.CertificateException;
23+
import java.security.cert.X509Certificate;
24+
import java.util.concurrent.TimeUnit;
25+
26+
import javax.annotation.PreDestroy;
27+
import javax.net.ssl.SSLContext;
28+
import javax.net.ssl.TrustManager;
29+
import javax.net.ssl.X509TrustManager;
30+
31+
import org.apache.commons.logging.Log;
32+
import org.apache.commons.logging.LogFactory;
33+
import org.apache.hc.client5.http.config.RequestConfig;
34+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
35+
import org.apache.hc.client5.http.impl.classic.HttpClients;
36+
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
37+
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
38+
import org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory;
39+
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
40+
import org.apache.hc.core5.http.io.SocketConfig;
41+
import org.apache.hc.core5.http.ssl.TLS;
42+
import org.apache.hc.core5.io.CloseMode;
43+
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
44+
import org.apache.hc.core5.pool.PoolReusePolicy;
45+
import org.apache.hc.core5.ssl.SSLContexts;
46+
import org.apache.hc.core5.util.TimeValue;
47+
import org.apache.hc.core5.util.Timeout;
48+
49+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
50+
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
51+
import org.springframework.context.annotation.Bean;
52+
import org.springframework.context.annotation.Configuration;
53+
54+
/**
55+
* Default configuration for {@link CloseableHttpClient}.
56+
*
57+
* @author Nguyen Ky Thanh
58+
*/
59+
@Configuration(proxyBeanMethods = false)
60+
@ConditionalOnMissingBean(CloseableHttpClient.class)
61+
public class HttpClient5FeignConfiguration {
62+
63+
private static final Log LOG = LogFactory.getLog(HttpClient5FeignConfiguration.class);
64+
65+
private CloseableHttpClient httpClient5;
66+
67+
@Bean
68+
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
69+
public HttpClientConnectionManager hc5ConnectionManager(
70+
FeignHttpClientProperties httpClientProperties) {
71+
return PoolingHttpClientConnectionManagerBuilder.create()
72+
.setSSLSocketFactory(httpsSSLConnectionSocketFactory(
73+
httpClientProperties.isDisableSslValidation()))
74+
.setMaxConnTotal(httpClientProperties.getMaxConnections())
75+
.setMaxConnPerRoute(httpClientProperties.getMaxConnectionsPerRoute())
76+
.setConnPoolPolicy(PoolReusePolicy.valueOf(
77+
httpClientProperties.getHc5().getPoolReusePolicy().name()))
78+
.setPoolConcurrencyPolicy(PoolConcurrencyPolicy.valueOf(
79+
httpClientProperties.getHc5().getPoolConcurrencyPolicy().name()))
80+
.setConnectionTimeToLive(
81+
TimeValue.of(httpClientProperties.getTimeToLive(),
82+
httpClientProperties.getTimeToLiveUnit()))
83+
.setDefaultSocketConfig(SocketConfig.custom()
84+
.setSoTimeout(Timeout.of(
85+
httpClientProperties.getHc5().getSocketTimeout(),
86+
httpClientProperties.getHc5().getSocketTimeoutUnit()))
87+
.build())
88+
.build();
89+
}
90+
91+
@Bean
92+
public CloseableHttpClient httpClient5(HttpClientConnectionManager connectionManager,
93+
FeignHttpClientProperties httpClientProperties) {
94+
httpClient5 = HttpClients.custom().disableCookieManagement().useSystemProperties()
95+
.setConnectionManager(connectionManager).evictExpiredConnections()
96+
.setDefaultRequestConfig(
97+
RequestConfig.custom()
98+
.setConnectTimeout(Timeout.of(
99+
httpClientProperties.getConnectionTimeout(),
100+
TimeUnit.MILLISECONDS))
101+
.setRedirectsEnabled(
102+
httpClientProperties.isFollowRedirects())
103+
.build())
104+
.build();
105+
return httpClient5;
106+
}
107+
108+
@PreDestroy
109+
public void destroy() {
110+
if (httpClient5 != null) {
111+
httpClient5.close(CloseMode.GRACEFUL);
112+
}
113+
}
114+
115+
private LayeredConnectionSocketFactory httpsSSLConnectionSocketFactory(
116+
boolean isDisableSslValidation) {
117+
final SSLConnectionSocketFactoryBuilder sslConnectionSocketFactoryBuilder = SSLConnectionSocketFactoryBuilder
118+
.create().setTlsVersions(TLS.V_1_3, TLS.V_1_2);
119+
120+
if (isDisableSslValidation) {
121+
try {
122+
final SSLContext sslContext = SSLContext.getInstance("SSL");
123+
sslContext.init(null,
124+
new TrustManager[] { new DisabledValidationTrustManager() },
125+
new SecureRandom());
126+
sslConnectionSocketFactoryBuilder.setSslContext(sslContext);
127+
}
128+
catch (NoSuchAlgorithmException e) {
129+
LOG.warn("Error creating SSLContext", e);
130+
}
131+
catch (KeyManagementException e) {
132+
LOG.warn("Error creating SSLContext", e);
133+
}
134+
}
135+
else {
136+
sslConnectionSocketFactoryBuilder
137+
.setSslContext(SSLContexts.createSystemDefault());
138+
}
139+
140+
return sslConnectionSocketFactoryBuilder.build();
141+
}
142+
143+
static class DisabledValidationTrustManager implements X509TrustManager {
144+
145+
DisabledValidationTrustManager() {
146+
}
147+
148+
public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
149+
throws CertificateException {
150+
}
151+
152+
public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
153+
throws CertificateException {
154+
}
155+
156+
public X509Certificate[] getAcceptedIssuers() {
157+
return null;
158+
}
159+
160+
}
161+
162+
}

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2020 the original author or authors.
2+
* Copyright 2013-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@
3838
* <code>spring.cloud.loadbalancer.ribbon.enabled</code> to <code>true</code>.
3939
*
4040
* @author Olga Maciaszek-Sharma
41+
* @author Nguyen Ky Thanh
4142
* @since 2.2.0
4243
*/
4344
@ConditionalOnClass(Feign.class)
@@ -51,6 +52,7 @@
5152
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
5253
@Import({ HttpClientFeignLoadBalancerConfiguration.class,
5354
OkHttpFeignLoadBalancerConfiguration.class,
55+
HttpClient5FeignLoadBalancerConfiguration.class,
5456
DefaultFeignLoadBalancerConfiguration.class })
5557
public class FeignLoadBalancerAutoConfiguration {
5658

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2013-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.openfeign.loadbalancer;
18+
19+
import java.util.List;
20+
21+
import feign.Client;
22+
import feign.hc5.ApacheHttp5Client;
23+
import org.apache.hc.client5.http.classic.HttpClient;
24+
25+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
26+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
27+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
28+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
29+
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
30+
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
31+
import org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration;
32+
import org.springframework.context.annotation.Bean;
33+
import org.springframework.context.annotation.Conditional;
34+
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.context.annotation.Import;
36+
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
37+
38+
/**
39+
* Configuration instantiating a {@link BlockingLoadBalancerClient}-based {@link Client}
40+
* object that uses {@link ApacheHttp5Client} under the hood.
41+
*
42+
* @author Nguyen Ky Thanh
43+
*/
44+
@Configuration(proxyBeanMethods = false)
45+
@ConditionalOnClass(ApacheHttp5Client.class)
46+
@ConditionalOnBean(BlockingLoadBalancerClient.class)
47+
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
48+
@Import(HttpClient5FeignConfiguration.class)
49+
class HttpClient5FeignLoadBalancerConfiguration {
50+
51+
@Bean
52+
@ConditionalOnMissingBean
53+
@Conditional(OnRetryNotEnabledCondition.class)
54+
public Client feignClient(BlockingLoadBalancerClient loadBalancerClient,
55+
HttpClient httpClient5) {
56+
Client delegate = new ApacheHttp5Client(httpClient5);
57+
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
58+
}
59+
60+
@Bean
61+
@ConditionalOnMissingBean
62+
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
63+
@ConditionalOnBean(LoadBalancedRetryFactory.class)
64+
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled",
65+
havingValue = "true", matchIfMissing = true)
66+
public Client feignRetryClient(BlockingLoadBalancerClient loadBalancerClient,
67+
HttpClient httpClient5,
68+
List<LoadBalancedRetryFactory> loadBalancedRetryFactories) {
69+
AnnotationAwareOrderComparator.sort(loadBalancedRetryFactories);
70+
Client delegate = new ApacheHttp5Client(httpClient5);
71+
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient,
72+
loadBalancedRetryFactories.get(0));
73+
}
74+
75+
}

0 commit comments

Comments
 (0)