diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc
index 338dcd5d..fef52d12 100644
--- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc
@@ -140,10 +140,10 @@ If none of them is on the classpath, the default feign client is used.
NOTE: `spring-cloud-starter-openfeign` supports `spring-cloud-starter-loadbalancer`. However, as is an optional dependency, you need to make sure it has been added to your project if you want to use it.
-The OkHttpClient and Apache HttpClient 5 Feign clients can be used by setting `spring.cloud.openfeign.okhttp.enabled` or `spring.cloud.openfeign.httpclient.hc5.enabled` to `true`, respectively, and having them on the classpath.
+The OkHttpClient, Apache HttpClient 5 and Http2Client Feign clients can be used by setting `spring.cloud.openfeign.okhttp.enabled` or `spring.cloud.openfeign.httpclient.hc5.enabled` or `spring.cloud.openfeign.http2client.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.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 and the ones prefixed with `httpclient.okhttp` to OkHttpClient. You can find a full list of properties you can customise in the appendix.
+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.
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/pom.xml b/spring-cloud-openfeign-core/pom.xml
index 99ae1089..cc62b647 100644
--- a/spring-cloud-openfeign-core/pom.xml
+++ b/spring-cloud-openfeign-core/pom.xml
@@ -133,6 +133,11 @@
feign-okhttp
true
+
+ io.github.openfeign
+ feign-java11
+ true
+
com.squareup.okhttp3
okhttp
diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java
index 90f6c4db..4fa906c8 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
@@ -17,6 +17,7 @@
package org.springframework.cloud.openfeign;
import java.lang.reflect.Method;
+import java.net.http.HttpClient;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
@@ -39,6 +40,7 @@ import feign.Client;
import feign.Feign;
import feign.Target;
import feign.hc5.ApacheHttp5Client;
+import feign.http2client.Http2Client;
import feign.okhttp.OkHttpClient;
import jakarta.annotation.PreDestroy;
import okhttp3.ConnectionPool;
@@ -383,6 +385,25 @@ public class FeignAutoConfiguration {
}
+ // the following configuration is for alternate feign clients if
+ // SC loadbalancer is not on the class path.
+ // see corresponding configurations in FeignLoadBalancerAutoConfiguration
+ // for load-balanced clients.
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass({ Http2Client.class, HttpClient.class })
+ @ConditionalOnMissingBean(HttpClient.class)
+ @ConditionalOnProperty("spring.cloud.openfeign.http2client.enabled")
+ @Import(org.springframework.cloud.openfeign.clientconfig.Http2ClientFeignConfiguration.class)
+ protected static class Http2ClientFeignConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(Client.class)
+ public Client feignClient(HttpClient httpClient) {
+ return new Http2Client(httpClient);
+ }
+
+ }
+
}
class FeignHints implements RuntimeHintsRegistrar {
diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/Http2ClientFeignConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/Http2ClientFeignConfiguration.java
new file mode 100644
index 00000000..f6106375
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/Http2ClientFeignConfiguration.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ * 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.net.http.HttpClient;
+import java.time.Duration;
+
+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 HttpClient}.
+ *
+ * @author changjin wei(魏昌进)
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnMissingBean(HttpClient.class)
+public class Http2ClientFeignConfiguration {
+
+ @Bean
+ public HttpClient httpClient(FeignHttpClientProperties httpClientProperties) {
+ return HttpClient.newBuilder()
+ .followRedirects(httpClientProperties.isFollowRedirects() ? HttpClient.Redirect.ALWAYS
+ : HttpClient.Redirect.NEVER)
+ .version(HttpClient.Version.valueOf(httpClientProperties.getHttp2().getVersion()))
+ .connectTimeout(Duration.ofMillis(httpClientProperties.getConnectionTimeout())).build();
+ }
+
+}
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 64216686..d760a68f 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-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.
@@ -54,7 +54,7 @@ import org.springframework.context.annotation.Import;
// see
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ OkHttpFeignLoadBalancerConfiguration.class, HttpClient5FeignLoadBalancerConfiguration.class,
- DefaultFeignLoadBalancerConfiguration.class })
+ Http2ClientFeignLoadBalancerConfiguration.class, DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {
@Bean
diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/Http2ClientFeignLoadBalancerConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/Http2ClientFeignLoadBalancerConfiguration.java
new file mode 100644
index 00000000..92f68586
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/Http2ClientFeignLoadBalancerConfiguration.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ * 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.net.http.HttpClient;
+import java.util.List;
+
+import feign.Client;
+import feign.http2client.Http2Client;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Configuration instantiating a {@link LoadBalancerClient}-based {@link Client} object
+ * that uses {@link Http2Client} under the hood.
+ *
+ * @author changjin wei(魏昌进)
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass({ Http2Client.class, HttpClient.class })
+@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
+@ConditionalOnProperty("spring.cloud.openfeign.http2client.enabled")
+@EnableConfigurationProperties(LoadBalancerClientsProperties.class)
+class Http2ClientFeignLoadBalancerConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @Conditional(OnRetryNotEnabledCondition.class)
+ public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
+ LoadBalancerClientFactory loadBalancerClientFactory,
+ List transformers) {
+ Client delegate = new Http2Client(httpClient);
+ return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancerClientFactory,
+ transformers);
+ }
+
+ @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(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
+ LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory,
+ List transformers) {
+ Client delegate = new Http2Client(httpClient);
+ return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
+ loadBalancerClientFactory, transformers);
+ }
+
+}
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 0e26cde1..ed8ef817 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
@@ -16,10 +16,12 @@
package org.springframework.cloud.openfeign.support;
+import java.net.http.HttpClient;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;
+import feign.http2client.Http2Client;
import feign.okhttp.OkHttpClient;
import okhttp3.Protocol;
@@ -100,6 +102,11 @@ public class FeignHttpClientProperties {
*/
private OkHttp okHttp = new OkHttp();
+ /**
+ * Additional {@link Http2Client}-specific properties.
+ */
+ private Http2Properties http2 = new Http2Properties();
+
public int getConnectionTimerRepeat() {
return connectionTimerRepeat;
}
@@ -180,6 +187,14 @@ public class FeignHttpClientProperties {
this.okHttp = okHttp;
}
+ public Http2Properties getHttp2() {
+ return http2;
+ }
+
+ public void setHttp2(Http2Properties http2) {
+ this.http2 = http2;
+ }
+
public static class Hc5Properties {
/**
@@ -362,4 +377,25 @@ public class FeignHttpClientProperties {
}
+ /**
+ * {@link Http2Client}-specific properties.
+ */
+ public static class Http2Properties {
+
+ /**
+ * Configure the protocols used by this client to communicate with remote servers.
+ * Uses {@link String} value of {@link HttpClient.Version}.
+ */
+ private String version = "HTTP_2";
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ }
+
}
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 5e91825d..4da8f3a6 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
@@ -44,6 +44,12 @@
"description": "Enables the use of the OK HTTP Client by Feign.",
"defaultValue": "false"
},
+ {
+ "name": "spring.cloud.openfeign.http2client.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Enables the use of the Java11 HTTP 2 Client by Feign.",
+ "defaultValue": "false"
+ },
{
"name": "spring.cloud.openfeign.compression.response.enabled",
"type": "java.lang.Boolean",
diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttp2ClientConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttp2ClientConfigurationTests.java
new file mode 100644
index 00000000..89e6022c
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttp2ClientConfigurationTests.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ * 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 java.net.http.HttpClient;
+import java.time.Duration;
+import java.util.Optional;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.context.ConfigurableApplicationContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author changjin wei(魏昌进)
+ */
+class FeignHttp2ClientConfigurationTests {
+
+ private ConfigurableApplicationContext context;
+
+ @BeforeEach
+ void setUp() {
+ context = new SpringApplicationBuilder()
+ .properties("debug=true", "spring.cloud.openfeign.http2client.enabled=true",
+ "spring.cloud.openfeign.httpclient.http2.version=HTTP_1_1",
+ "spring.cloud.openfeign.httpclient.connectionTimeout=15")
+ .web(WebApplicationType.NONE).sources(FeignAutoConfiguration.class).run();
+ }
+
+ @AfterEach
+ void tearDown() {
+ if (context != null) {
+ context.close();
+ }
+ }
+
+ @Test
+ void shouldConfigureConnectTimeout() {
+ HttpClient httpClient = context.getBean(HttpClient.class);
+
+ assertThat(httpClient.connectTimeout()).isEqualTo(Optional.ofNullable(Duration.ofMillis(15)));
+ }
+
+ @Test
+ void shouldResolveVersionFromProperties() {
+ HttpClient httpClient = context.getBean(HttpClient.class);
+
+ assertThat(httpClient.version()).isEqualTo(HttpClient.Version.HTTP_1_1);
+ }
+
+}
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 18b402f8..8964a95e 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-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.
@@ -16,10 +16,12 @@
package org.springframework.cloud.openfeign.loadbalancer;
+import java.net.http.HttpClient;
import java.util.Map;
import feign.Client;
import feign.hc5.ApacheHttp5Client;
+import feign.http2client.Http2Client;
import feign.okhttp.OkHttpClient;
import org.junit.jupiter.api.Test;
@@ -37,6 +39,7 @@ import static org.springframework.test.util.ReflectionTestUtils.getField;
/**
* @author Olga Maciaszek-Sharma
* @author Nguyen Ky Thanh
+ * @author changjin wei(魏昌进)
*/
class FeignLoadBalancerAutoConfigurationTests {
@@ -65,6 +68,21 @@ class FeignLoadBalancerAutoConfigurationTests {
}
+ @Test
+ void shouldInstantiateHttp2ClientFeignClientWhenEnabled() {
+ ConfigurableApplicationContext context = initContext("spring.cloud.openfeign.httpclient.hc5.enabled=false",
+ "spring.cloud.openfeign.http2client.enabled=true", "spring.cloud.loadbalancer.retry.enabled=false");
+ assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
+ Map beans = context
+ .getBeansOfType(FeignBlockingLoadBalancerClient.class);
+ assertThat(beans).as("Missing bean of type %s", Http2Client.class).hasSize(1);
+ Client client = beans.get("feignClient").getDelegate();
+ assertThat(client).isInstanceOf(Http2Client.class);
+ Http2Client http2Client = (Http2Client) client;
+ HttpClient httpClient = (HttpClient) getField(http2Client, "client");
+ assertThat(httpClient).isInstanceOf(HttpClient.class);
+ }
+
@Test
void shouldInstantiateHttpFeignClient5WhenAvailableAndOkHttpDisabled() {
ConfigurableApplicationContext context = initContext("spring.cloud.openfeign.okhttp.enabled=false",
@@ -73,6 +91,14 @@ class FeignLoadBalancerAutoConfigurationTests {
assertLoadBalanced(context, ApacheHttp5Client.class);
}
+ @Test
+ void shouldInstantiateHttpFeignClient5WhenAvailableAndHttp2ClientDisabled() {
+ ConfigurableApplicationContext context = initContext("spring.cloud.openfeign.http2client.enabled=false",
+ "spring.cloud.loadbalancer.retry.enabled=false");
+ assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
+ assertLoadBalanced(context, ApacheHttp5Client.class);
+ }
+
@Test
void shouldInstantiateRetryableDefaultFeignBlockingLoadBalancerClientWhenHttpClientDisabled() {
ConfigurableApplicationContext context = initContext("spring.cloud.openfeign.httpclient.hc5.enabled=false");
@@ -88,6 +114,14 @@ class FeignLoadBalancerAutoConfigurationTests {
assertLoadBalancedWithRetries(context, OkHttpClient.class);
}
+ @Test
+ void shouldInstantiateRetryableHttp2ClientFeignClientWhenEnabled() {
+ ConfigurableApplicationContext context = initContext("spring.cloud.openfeign.httpclient.hc5.enabled=false",
+ "spring.cloud.openfeign.http2client.enabled=true");
+ assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
+ assertLoadBalancedWithRetries(context, Http2Client.class);
+ }
+
@Test
void shouldInstantiateRetryableHttpFeignClient5WhenEnabled() {
ConfigurableApplicationContext context = initContext();
diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/test/Http2ClientConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/test/Http2ClientConfigurationTests.java
new file mode 100644
index 00000000..d51a0349
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/test/Http2ClientConfigurationTests.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ * 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.test;
+
+import java.net.http.HttpClient;
+
+import feign.Client;
+import feign.http2client.Http2Client;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author changjin wei(魏昌进)
+ */
+@SpringBootTest(properties = { "spring.cloud.openfeign.http2client.enabled= true",
+ "spring.cloud.openfeign.httpclient.hc5.enabled= false", "spring.cloud.loadbalancer.retry.enabled= false" })
+@DirtiesContext
+class Http2ClientConfigurationTests {
+
+ @Autowired
+ FeignBlockingLoadBalancerClient feignClient;
+
+ private static final HttpClient defaultHttpClient = HttpClient.newHttpClient();
+
+ @Test
+ void shouldInstantiateFeignHttp2Client() {
+ Client delegate = feignClient.getDelegate();
+ assertThat(delegate instanceof Http2Client).isTrue();
+ Http2Client http2Client = (Http2Client) delegate;
+ HttpClient httpClient = getField(http2Client, "client");
+ assertThat(httpClient).isEqualTo(defaultHttpClient);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected T getField(Object target, String name) {
+ Object value = ReflectionTestUtils.getField(target, target.getClass(), name);
+ return (T) value;
+ }
+
+ @FeignClient(name = "foo")
+ interface FooClient {
+
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ static class TestConfig {
+
+ @Bean
+ public HttpClient client() {
+ return defaultHttpClient;
+ }
+
+ }
+
+}
diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/valid/FeignHttp2ClientTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/valid/FeignHttp2ClientTests.java
new file mode 100644
index 00000000..81c9db69
--- /dev/null
+++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/valid/FeignHttp2ClientTests.java
@@ -0,0 +1,256 @@
+/*
+ * 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.
+ * 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.valid;
+
+import java.util.Objects;
+
+import feign.Client;
+import feign.http2client.Http2Client;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.cloud.client.DefaultServiceInstance;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
+import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author changjin wei(魏昌进)
+ */
+@SpringBootTest(classes = FeignHttp2ClientTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
+ value = { "spring.application.name=feignclienttest", "spring.cloud.openfeign.circuitbreaker.enabled=false",
+ "spring.cloud.openfeign.httpclient.hc5.enabled=false",
+ "spring.cloud.openfeign.http2client.enabled=true", "spring.cloud.loadbalancer.retry.enabled=false" })
+@DirtiesContext
+class FeignHttp2ClientTests {
+
+ @Autowired
+ private TestClient testClient;
+
+ @Autowired
+ private Client feignClient;
+
+ @Autowired
+ private UserClient userClient;
+
+ @Test
+ void testSimpleType() {
+ Hello hello = testClient.getHello();
+ assertThat(hello).as("hello was null").isNotNull();
+ assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
+ }
+
+ @Test
+ void testPatch() {
+ ResponseEntity response = testClient.patchHello(new Hello("foo"));
+ assertThat(response).isNotNull();
+ String header = response.getHeaders().getFirst("x-hello");
+ assertThat(header).isEqualTo("hello world patch");
+ }
+
+ @Test
+ void testFeignClientType() {
+ assertThat(feignClient).isInstanceOf(FeignBlockingLoadBalancerClient.class);
+ FeignBlockingLoadBalancerClient client = (FeignBlockingLoadBalancerClient) feignClient;
+ Client delegate = client.getDelegate();
+ assertThat(delegate).isInstanceOf(Http2Client.class);
+ }
+
+ @Test
+ void testFeignInheritanceSupport() {
+ assertThat(userClient).as("UserClient was null").isNotNull();
+ final User user = userClient.getUser(1);
+ assertThat(user).as("Returned user was null").isNotNull();
+ assertThat(new User("John Smith")).as("Users were different").isEqualTo(user);
+ }
+
+ @FeignClient("localapp")
+ protected interface TestClient extends BaseTestClient {
+
+ }
+
+ protected interface BaseTestClient {
+
+ @GetMapping("/hello")
+ Hello getHello();
+
+ @PatchMapping(value = "/hellop", consumes = "application/json")
+ ResponseEntity patchHello(Hello hello);
+
+ }
+
+ protected interface UserService {
+
+ @GetMapping("/users/{id}")
+ User getUser(@PathVariable("id") long id);
+
+ }
+
+ @FeignClient("localapp1")
+ protected interface UserClient extends UserService {
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @EnableAutoConfiguration
+ @RestController
+ @EnableFeignClients(clients = { TestClient.class, UserClient.class })
+ @LoadBalancerClients({
+ @LoadBalancerClient(name = "localapp", configuration = FeignHttpClientTests.LocalClientConfiguration.class),
+ @LoadBalancerClient(name = "localapp1",
+ configuration = FeignHttpClientTests.LocalClientConfiguration.class) })
+ @Import(NoSecurityConfiguration.class)
+ protected static class Application implements UserService {
+
+ @GetMapping("/hello")
+ public Hello getHello() {
+ return new Hello("hello world 1");
+ }
+
+ @PatchMapping("/hellop")
+ public ResponseEntity patchHello(@RequestBody Hello hello,
+ @RequestHeader("Content-Length") int contentLength) {
+ if (contentLength <= 0) {
+ throw new IllegalArgumentException("Invalid Content-Length " + contentLength);
+ }
+ if (!hello.getMessage().equals("foo")) {
+ throw new IllegalArgumentException("Invalid Hello: " + hello.getMessage());
+ }
+ return ResponseEntity.ok().header("X-Hello", "hello world patch").build();
+ }
+
+ @Override
+ public User getUser(@PathVariable("id") long id) {
+ return new User("John Smith");
+ }
+
+ }
+
+ public static class Hello {
+
+ private String message;
+
+ Hello() {
+ }
+
+ Hello(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Hello that = (Hello) o;
+ return Objects.equals(message, that.message);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(message);
+ }
+
+ }
+
+ public static class User {
+
+ private String name;
+
+ User() {
+ }
+
+ User(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ User that = (User) o;
+ return Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+
+ }
+
+ // Load balancer with fixed server list for "local" pointing to localhost
+ @Configuration(proxyBeanMethods = false)
+ static class LocalClientConfiguration {
+
+ @LocalServerPort
+ private int port = 0;
+
+ @Bean
+ public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
+ return ServiceInstanceListSuppliers.from("local",
+ new DefaultServiceInstance("local-1", "local", "localhost", port, false));
+ }
+
+ }
+
+}