diff --git a/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc b/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc index 7181a6e6f..8b25fdfb1 100644 --- a/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc @@ -139,6 +139,7 @@ The OkHttpClient, Apache HttpClient 5 and Http2Client Feign clients can be used You can customize the HTTP client used by providing a bean of either `org.apache.hc.client5.http.impl.classic.CloseableHttpClient` when using Apache HC5. You can further customise http clients by setting values in the `spring.cloud.openfeign.httpclient.xxx` properties. The ones prefixed just with `httpclient` will work for all the clients, the ones prefixed with `httpclient.hc5` to Apache HttpClient 5, the ones prefixed with `httpclient.okhttp` to OkHttpClient and the ones prefixed with `httpclient.http2` to Http2Client. You can find a full list of properties you can customise in the appendix. +If you can not configure Apache HttpClient 5 by using properties, there is an `HttpClientBuilderCustomizer` interface for programmatic configuration. TIP: Starting with Spring Cloud OpenFeign 4, the Feign Apache HttpClient 4 is no longer supported. We suggest using Apache HttpClient 5 instead. 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 index 36ff6b1d1..56338aef8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2023 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. @@ -20,6 +20,7 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.X509Certificate; +import java.util.List; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLContext; @@ -31,6 +32,7 @@ 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.HttpClientBuilder; 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; @@ -46,6 +48,7 @@ import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; import org.springframework.context.annotation.Bean; @@ -56,6 +59,7 @@ * * @author Nguyen Ky Thanh * @author changjin wei(魏昌进) + * @author Kwangyong Kim */ @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(CloseableHttpClient.class) @@ -85,18 +89,23 @@ public HttpClientConnectionManager hc5ConnectionManager(FeignHttpClientPropertie @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()) - .setConnectionRequestTimeout( - Timeout.of(httpClientProperties.getHc5().getConnectionRequestTimeout(), - httpClientProperties.getHc5().getConnectionRequestTimeoutUnit())) - .build()) - .build(); + FeignHttpClientProperties httpClientProperties, + ObjectProvider> customizerProvider) { + HttpClientBuilder httpClientBuilder = HttpClients.custom().disableCookieManagement().useSystemProperties() + .setConnectionManager(connectionManager).evictExpiredConnections() + .setDefaultRequestConfig(RequestConfig.custom() + .setConnectTimeout( + Timeout.of(httpClientProperties.getConnectionTimeout(), TimeUnit.MILLISECONDS)) + .setRedirectsEnabled(httpClientProperties.isFollowRedirects()) + .setConnectionRequestTimeout( + Timeout.of(httpClientProperties.getHc5().getConnectionRequestTimeout(), + httpClientProperties.getHc5().getConnectionRequestTimeoutUnit())) + .build()); + + customizerProvider.getIfAvailable(List::of) + .forEach(c -> c.customize(httpClientBuilder)); + + httpClient5 = httpClientBuilder.build(); return httpClient5; } @@ -146,4 +155,19 @@ public X509Certificate[] getAcceptedIssuers() { } + /** + * Callback interface that customize {@link HttpClientBuilder} objects before HttpClient created. + * + * @author Kwangyong Kim + * @since 4.1.0 + */ + public interface HttpClientBuilderCustomizer { + + /** + * Customize HttpClientBuilder. + * @param builder the {@code HttpClientBuilder} to customize + */ + void customize(HttpClientBuilder builder); + + } } 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 index 937a03e18..982a0a218 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2023 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,21 +19,29 @@ import feign.Client; import feign.hc5.ApacheHttp5Client; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration.HttpClientBuilderCustomizer; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; /** * @author Nguyen Ky Thanh * @author Olga Maciaszek-Sharma + * @author Kwangyong Kim */ class FeignHttpClient5ConfigurationTests { @@ -72,4 +80,28 @@ void shouldNotInstantiateHttpClient5ByWhenDependenciesPresentButPropertyDisabled } } + @Test + void shouldInstantiateHttpClient5ByUsingHttpClientBuilderCustomizer() { + ConfigurableApplicationContext context = new SpringApplicationBuilder() + .web(WebApplicationType.NONE) + .sources(FeignAutoConfiguration.class, Config.class) + .run(); + + CloseableHttpClient httpClient = context.getBean(CloseableHttpClient.class); + assertThat(httpClient).isNotNull(); + HttpClientBuilderCustomizer customizer = context.getBean(HttpClientBuilderCustomizer.class); + verify(customizer).customize(any(HttpClientBuilder.class)); + + if (context != null) { + context.close(); + } + } + + @Configuration + static class Config { + @Bean + HttpClientBuilderCustomizer customizer() { + return Mockito.mock(HttpClientBuilderCustomizer.class); + } + } }