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() {