Describe the bug I use spring cloud openfeign with version 2.1.2RELEASE. I find in some cases httpClient config will be invalid.
why
I guess reason why config invalid is FeignAutoConfiguration or HttpClientFeignLoadBalancedConfiguration class use ConditionalOnMissingBean annotation.
all CloseableHttpClientis valid if no CloseableHttpClient type bean, because the code blow:
// must no CloseableHttpClient bean,that openfeign config can valid
@ConditionalOnMissingBean(CloseableHttpClient.class)
protected static class HttpClientFeignConfiguration {
private CloseableHttpClient httpClient;
@Autowired(required = false)
private RegistryBuilder registryBuilder;
@Bean
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
public HttpClientConnectionManager connectionManager(
ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(httpClientProperties.isDisableSslValidation(),
httpClientProperties.getMaxConnections(),
httpClientProperties.getMaxConnectionsPerRoute(),
httpClientProperties.getTimeToLive(),
httpClientProperties.getTimeToLiveUnit(),
this.registryBuilder);
this.connectionManagerTimer.schedule(new TimerTask() {
@Override
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000, httpClientProperties.getConnectionTimerRepeat());
return connectionManager;
}
@Bean
@ConditionalOnProperty(value = "feign.compression.response.enabled", havingValue = "true")
public CloseableHttpClient customHttpClient(
HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {
HttpClientBuilder builder = HttpClientBuilder.create()
.disableCookieManagement().useSystemProperties();
this.httpClient = createClient(builder, httpClientConnectionManager,
httpClientProperties);
return this.httpClient;
}
@Bean
@ConditionalOnProperty(value = "feign.compression.response.enabled", havingValue = "false", matchIfMissing = true)
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {
this.httpClient = createClient(httpClientFactory.createBuilder(),
httpClientConnectionManager, httpClientProperties);
return this.httpClient;
}
private CloseableHttpClient createClient(HttpClientBuilder builder,
HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {
RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(httpClientProperties.getConnectionTimeout())
.setRedirectsEnabled(httpClientProperties.isFollowRedirects())
.build();
CloseableHttpClient httpClient = builder
.setDefaultRequestConfig(defaultRequestConfig)
.setConnectionManager(httpClientConnectionManager).build();
return httpClient;
}
@PreDestroy
public void destroy() throws Exception {
this.connectionManagerTimer.cancel();
if (this.httpClient != null) {
this.httpClient.close();
}
}
}
if In some condition I need inject a custom CloseableHttpClient to spring context, the openfeign httpClient will be ignored.
Comment From: OlgaMaciaszek
Hello, @SSchrodingerCat 2.1.x is no longer supported. 3.1.x and 4.0.x are currently supported. Please verify if the issue still persists on a supported version. If yes, please provide a minimal, complete, verifiable example that reproduces the issue.
Comment From: SSchrodingerCat
@OlgaMaciaszek Thank for reply.
I test 3.1.x version, find we may override HttpClientConnectionManager config Unintentionally.
example
here is a simple example:
pom file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>openfeignBug</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>openfeignBug</name>
<description>openfeignBug</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>10.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
starter
package com.example.openfeignbug;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableFeignClients(clients = {TestFeignClient.class})
public class OpenfeignBugApplication {
public static void main(String[] args) {
SpringApplication.run(OpenfeignBugApplication.class, args);
}
@Bean
public HttpClientConnectionManager httpClientConnectionManager() {
return new BasicHttpClientConnectionManager();
}
}
Controller
package com.example.openfeignbug;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private TestFeignClient testFeignClient;
@GetMapping("/test")
public String test() {
return testFeignClient.test();
}
}
FeignClient
package com.example.openfeignbug;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "name", url = "127.0.0.1", contextId = "contextId")
public interface TestFeignClient {
@GetMapping("")
String test();
}
application.yml
feign:
httpclient:
enabled: true
max-connections: 100
max-connections-per-route: 100
reason
reason why is that only HttpClientConnectionManager not exist that spring can inject HttpClientConnectionManager instance to its context.
if we create bean with type HttpClientConnectionManager, feignClient config will ignore.
@ConditionalOnMissingBean(HttpClientConnectionManager.class) in FeignAutoConfiguration line 236.
sugggestion
i have two idea to resolve it.
bean name instead of bean type
I think we can use bean name instead of bean type injecting HttpClientConnectionManager instance, like:
@Conditional({HttpClient5DisabledConditions.class})
protected static class HttpClientFeignConfiguration {
private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer", true);
@Autowired(
required = false
)
private RegistryBuilder registryBuilder;
private org.apache.http.impl.client.CloseableHttpClient httpClient;
protected HttpClientFeignConfiguration() {
}
// define httpClientConnectionManager a specific name
// must load HttpClientConnectionManager
@Bean("specific-name")
public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory, FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
this.connectionManagerTimer.schedule(new TimerTask() {
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000L, (long)httpClientProperties.getConnectionTimerRepeat());
return connectionManager;
}
@Bean
public org.apache.http.impl.client.CloseableHttpClient httpClient(
ApacheHttpClientFactory httpClientFactory,
// use Qualifier annotation inject bean instance
@Qualifier("specific-name") HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {
RequestConfig defaultRequestConfig = RequestConfig.custom().setConnectTimeout(httpClientProperties.getConnectionTimeout()).setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
this.httpClient = httpClientFactory.createBuilder().setConnectionManager(httpClientConnectionManager).setDefaultRequestConfig(defaultRequestConfig).build();
return this.httpClient;
}
@Bean
@ConditionalOnMissingBean({Client.class})
public Client feignClient(HttpClient httpClient) {
return new ApacheHttpClient(httpClient);
}
@PreDestroy
public void destroy() {
this.connectionManagerTimer.cancel();
if (this.httpClient != null) {
try {
this.httpClient.close();
} catch (IOException var2) {
if (FeignAutoConfiguration.LOG.isErrorEnabled()) {
FeignAutoConfiguration.LOG.error("Could not correctly close httpClient.");
}
}
}
}
}
wrap manager to a new type
we can wrap HttpClientConnectionManager in to a new instance, like HttpClientConnectionManagerWrapper, and use HttpClientConnectionManagerWrapper to check if we can inject HttpClientConnectionManager.
like:
public class HttpClientConnectionManagerWrapper implements HttpClientConnectionManager, Closeable {
public HttpClientConnectionManagerWrapper(HttpClientConnectionManager instance) {
manager = instance;
}
// some agent function
HttpClientConnectionManager manager;
}
// in FeignAutoConfiguration class
@Bean
@ConditionalOnMissingBean({HttpClientConnectionManagerWrapper.class})
public HttpClientConnectionManagerWrapper connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory, FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
this.connectionManagerTimer.schedule(new TimerTask() {
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000L, (long)httpClientProperties.getConnectionTimerRepeat());
return new HttpClientConnectionManagerWrapper(connectionManager);
}
@Bean
public org.apache.http.impl.client.CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory, HttpClientConnectionManagerWrapper httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {
RequestConfig defaultRequestConfig = RequestConfig.custom().setConnectTimeout(httpClientProperties.getConnectionTimeout()).setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
this.httpClient = httpClientFactory.createBuilder().setConnectionManager(httpClientConnectionManager).setDefaultRequestConfig(defaultRequestConfig).build();
return this.httpClient;
}
Comment From: OlgaMaciaszek
Hello @SSchrodingerCat . Thanks for the code and sorry for not getting back to you earlier on this. This is actually done on purpose so that the users can override our setup with their custom-created beans. If you want to use your own bean, but still reuse some of our functionality, it will be up to you to wrap it or copy the appropriate config into your bean.
Comment From: spring-cloud-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: spring-cloud-issues
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.