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)); + } + + } + +}