From 6996e8a35e2b7337f12664526167a1c531891cca Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Fri, 12 Mar 2021 23:49:52 +0100 Subject: [PATCH 1/6] Adding async feign with AsyncDefault client underlying. --- .../cloud/openfeign/FeignClient.java | 5 + .../openfeign/FeignClientFactoryBean.java | 215 +++++++++++++++++- .../openfeign/FeignClientsRegistrar.java | 3 +- .../async/AsyncFeignAutoConfiguration.java | 59 +++++ .../cloud/openfeign/async/AsyncTargeter.java | 33 +++ .../openfeign/async/DefaultAsyncTargeter.java | 36 +++ .../AsyncFeignClientFactoryTests.java | 158 +++++++++++++ .../openfeign/FeignClientBuilderTests.java | 4 +- 8 files changed, 503 insertions(+), 10 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncTargeter.java create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/DefaultAsyncTargeter.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClient.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClient.java index f1536159c..58511c24e 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClient.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClient.java @@ -145,4 +145,9 @@ */ boolean primary() default true; + /** + * @return whether Feign will use an underlying async Http client. + */ + boolean asynchronous() default false; + } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java index 8d61d127f..bcd79e1cc 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java @@ -22,6 +22,9 @@ import java.util.Objects; import java.util.concurrent.TimeUnit; +import feign.AsyncClient; +import feign.AsyncFeign; +import feign.AsyncFeign.AsyncBuilder; import feign.Client; import feign.Contract; import feign.ExceptionPropagationPolicy; @@ -43,6 +46,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.cloud.openfeign.async.AsyncTargeter; import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer; import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient; import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient; @@ -99,6 +103,8 @@ public class FeignClientFactoryBean implements FactoryBean, Initializing private boolean followRedirects = new Request.Options().isFollowRedirects(); + private boolean asynchronous = false; + @Override public void afterPropertiesSet() { Assert.hasText(contextId, "Context id must be set"); @@ -124,6 +130,24 @@ protected Feign.Builder feign(FeignContext context) { return builder; } + protected AsyncBuilder asyncFeign(FeignContext context) { + FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); + Logger logger = loggerFactory.create(type); + + // @formatter:off + AsyncBuilder builder = get(context, AsyncBuilder.class) + // required values + .logger(logger) + .encoder(get(context, Encoder.class)) + .decoder(get(context, Decoder.class)) + .contract(get(context, Contract.class)); + // @formatter:on + + configureFeign(context, builder); + + return builder; + } + private void applyBuildCustomizers(FeignContext context, Feign.Builder builder) { Map customizerMap = context .getInstances(contextId, FeignBuilderCustomizer.class); @@ -166,6 +190,36 @@ protected void configureFeign(FeignContext context, Feign.Builder builder) { } } + protected void configureFeign(FeignContext context, AsyncBuilder builder) { + FeignClientProperties properties = beanFactory != null + ? beanFactory.getBean(FeignClientProperties.class) + : applicationContext.getBean(FeignClientProperties.class); + + FeignClientConfigurer feignClientConfigurer = getOptional(context, + FeignClientConfigurer.class); + setInheritParentContext(feignClientConfigurer.inheritParentConfiguration()); + + if (properties != null && inheritParentContext) { + if (properties.isDefaultToProperties()) { + configureUsingConfiguration(context, builder); + configureUsingProperties( + properties.getConfig().get(properties.getDefaultConfig()), + builder); + configureUsingProperties(properties.getConfig().get(contextId), builder); + } + else { + configureUsingProperties( + properties.getConfig().get(properties.getDefaultConfig()), + builder); + configureUsingProperties(properties.getConfig().get(contextId), builder); + configureUsingConfiguration(context, builder); + } + } + else { + configureUsingConfiguration(context, builder); + } + } + protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) { Logger.Level level = getInheritedAwareOptional(context, Logger.Level.class); @@ -220,6 +274,51 @@ protected void configureUsingConfiguration(FeignContext context, } } + protected void configureUsingConfiguration(FeignContext context, + AsyncBuilder builder) { + Logger.Level level = getInheritedAwareOptional(context, Logger.Level.class); + if (level != null) { + builder.logLevel(level); + } + ErrorDecoder errorDecoder = getInheritedAwareOptional(context, + ErrorDecoder.class); + if (errorDecoder != null) { + builder.errorDecoder(errorDecoder); + } + else { + FeignErrorDecoderFactory errorDecoderFactory = getOptional(context, + FeignErrorDecoderFactory.class); + if (errorDecoderFactory != null) { + ErrorDecoder factoryErrorDecoder = errorDecoderFactory.create(type); + builder.errorDecoder(factoryErrorDecoder); + } + } + Request.Options options = getInheritedAwareOptional(context, + Request.Options.class); + if (options != null) { + builder.options(options); + readTimeoutMillis = options.readTimeoutMillis(); + connectTimeoutMillis = options.connectTimeoutMillis(); + followRedirects = options.isFollowRedirects(); + } + Map requestInterceptors = getInheritedAwareInstances( + context, RequestInterceptor.class); + if (requestInterceptors != null) { + List interceptors = new ArrayList<>( + requestInterceptors.values()); + AnnotationAwareOrderComparator.sort(interceptors); + builder.requestInterceptors(interceptors); + } + QueryMapEncoder queryMapEncoder = getInheritedAwareOptional(context, + QueryMapEncoder.class); + if (queryMapEncoder != null) { + builder.queryMapEncoder(queryMapEncoder); + } + if (decode404) { + builder.decode404(); + } + } + protected void configureUsingProperties( FeignClientProperties.FeignClientConfiguration config, Feign.Builder builder) { @@ -293,6 +392,69 @@ protected void configureUsingProperties( } } + protected void configureUsingProperties( + FeignClientProperties.FeignClientConfiguration config, AsyncBuilder builder) { + if (config == null) { + return; + } + + if (config.getLoggerLevel() != null) { + builder.logLevel(config.getLoggerLevel()); + } + + connectTimeoutMillis = config.getConnectTimeout() != null + ? config.getConnectTimeout() : connectTimeoutMillis; + readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() + : readTimeoutMillis; + followRedirects = config.isFollowRedirects() != null ? config.isFollowRedirects() + : followRedirects; + + builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, + readTimeoutMillis, TimeUnit.MILLISECONDS, followRedirects)); + + if (config.getErrorDecoder() != null) { + ErrorDecoder errorDecoder = getOrInstantiate(config.getErrorDecoder()); + builder.errorDecoder(errorDecoder); + } + + if (config.getRequestInterceptors() != null + && !config.getRequestInterceptors().isEmpty()) { + // this will add request interceptor to builder, not replace existing + for (Class bean : config.getRequestInterceptors()) { + RequestInterceptor interceptor = getOrInstantiate(bean); + builder.requestInterceptor(interceptor); + } + } + + if (config.getDecode404() != null) { + if (config.getDecode404()) { + builder.decode404(); + } + } + + if (Objects.nonNull(config.getEncoder())) { + builder.encoder(getOrInstantiate(config.getEncoder())); + } + + if (Objects.nonNull(config.getDefaultRequestHeaders())) { + builder.requestInterceptor(requestTemplate -> requestTemplate + .headers(config.getDefaultRequestHeaders())); + } + + if (Objects.nonNull(config.getDefaultQueryParameters())) { + builder.requestInterceptor(requestTemplate -> requestTemplate + .queries(config.getDefaultQueryParameters())); + } + + if (Objects.nonNull(config.getDecoder())) { + builder.decoder(getOrInstantiate(config.getDecoder())); + } + + if (Objects.nonNull(config.getContract())) { + builder.contract(getOrInstantiate(config.getContract())); + } + } + private T getOrInstantiate(Class tClass) { try { return beanFactory != null ? beanFactory.getBean(tClass) @@ -350,6 +512,9 @@ protected T loadBalance(Feign.Builder builder, FeignContext context, @Override public Object getObject() { + if (asynchronous) { + return getAsyncTarget(); + } return getTarget(); } @@ -404,6 +569,33 @@ T getTarget() { new HardCodedTarget<>(type, name, url)); } + /** + * @param the target type of the Feign client + * @return an {@link AsyncFeign} client created with the specified data and the + * context information + */ + T getAsyncTarget() { + FeignContext context = beanFactory != null + ? beanFactory.getBean(FeignContext.class) + : applicationContext.getBean(FeignContext.class); + AsyncBuilder builder = asyncFeign(context); + + if (!StringUtils.hasText(url)) { + url = name; + } + if (StringUtils.hasText(url) && !url.startsWith("http")) { + url = "http://" + url; + } + String url = this.url + cleanPath(); + AsyncClient client = getOptional(context, AsyncClient.class); + if (client != null) { + builder.client(client); + } + AsyncTargeter targeter = get(context, AsyncTargeter.class); + return (T) targeter.target(this, builder, context, + new HardCodedTarget<>(type, name, url)); + } + private String cleanPath() { String path = this.path.trim(); if (StringUtils.hasLength(path)) { @@ -509,6 +701,14 @@ public void setFallbackFactory(Class fallbackFactory) { this.fallbackFactory = fallbackFactory; } + public boolean isAsynchronous() { + return asynchronous; + } + + public void setAsynchronous(boolean asynchronous) { + this.asynchronous = asynchronous; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -528,14 +728,15 @@ public boolean equals(Object o) { && Objects.equals(type, that.type) && Objects.equals(url, that.url) && Objects.equals(connectTimeoutMillis, that.connectTimeoutMillis) && Objects.equals(readTimeoutMillis, that.readTimeoutMillis) - && Objects.equals(followRedirects, that.followRedirects); + && Objects.equals(followRedirects, that.followRedirects) + && Objects.equals(asynchronous, that.asynchronous); } @Override public int hashCode() { return Objects.hash(applicationContext, beanFactory, decode404, inheritParentContext, fallback, fallbackFactory, name, path, type, url, - readTimeoutMillis, connectTimeoutMillis, followRedirects); + readTimeoutMillis, connectTimeoutMillis, followRedirects, asynchronous); } @Override @@ -548,11 +749,11 @@ public String toString() { .append("applicationContext=").append(applicationContext).append(", ") .append("beanFactory=").append(beanFactory).append(", ") .append("fallback=").append(fallback).append(", ") - .append("fallbackFactory=").append(fallbackFactory).append("}") - .append("connectTimeoutMillis=").append(connectTimeoutMillis).append("}") - .append("readTimeoutMillis=").append(readTimeoutMillis).append("}") - .append("followRedirects=").append(followRedirects).append("}") - .toString(); + .append("fallbackFactory=").append(fallbackFactory).append(", ") + .append("connectTimeoutMillis=").append(connectTimeoutMillis).append(", ") + .append("readTimeoutMillis=").append(readTimeoutMillis).append(", ") + .append("followRedirects=").append(followRedirects).append(", ") + .append("asynchronous=").append(asynchronous).append("}").toString(); } @Override diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java index 42d4d35cd..706f163ce 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java @@ -219,11 +219,13 @@ private void registerFeignClient(BeanDefinitionRegistry registry, ? (ConfigurableBeanFactory) registry : null; String contextId = getContextId(beanFactory, attributes); String name = getName(attributes); + boolean asynchronous = (Boolean) attributes.get("asynchronous"); FeignClientFactoryBean factoryBean = new FeignClientFactoryBean(); factoryBean.setBeanFactory(beanFactory); factoryBean.setName(name); factoryBean.setContextId(contextId); factoryBean.setType(clazz); + factoryBean.setAsynchronous(asynchronous); BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(clazz, () -> { factoryBean.setUrl(getUrl(beanFactory, attributes)); @@ -255,7 +257,6 @@ private void registerFeignClient(BeanDefinitionRegistry registry, // has a default, won't be null boolean primary = (Boolean) attributes.get("primary"); - beanDefinition.setPrimary(primary); String[] qualifiers = getQualifiers(attributes); diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java new file mode 100644 index 000000000..fe716df12 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java @@ -0,0 +1,59 @@ +/* + * 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.async; + +import feign.AsyncFeign; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.openfeign.FeignClientProperties; +import org.springframework.cloud.openfeign.FeignClientsConfiguration; +import org.springframework.cloud.openfeign.support.FeignEncoderProperties; +import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Scope; + +@ConditionalOnClass(AsyncFeign.class) +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({ FeignClientProperties.class, + FeignHttpClientProperties.class, FeignEncoderProperties.class }) +@Import(FeignClientsConfiguration.class) +public class AsyncFeignAutoConfiguration { + + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + @ConditionalOnMissingBean + public AsyncFeign.AsyncBuilder asyncFeignBuilder() { + return AsyncFeign.asyncBuilder(); + } + + @Configuration(proxyBeanMethods = false) + protected static class DefaultAsyncFeignTargeterConfiguration { + + @Bean + @ConditionalOnMissingBean + public AsyncTargeter asyncTargeter() { + return new DefaultAsyncTargeter(); + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncTargeter.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncTargeter.java new file mode 100644 index 000000000..7a525069f --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncTargeter.java @@ -0,0 +1,33 @@ +/* + * 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.async; + +import feign.AsyncFeign.AsyncBuilder; +import feign.Target; + +import org.springframework.cloud.openfeign.FeignClientFactoryBean; +import org.springframework.cloud.openfeign.FeignContext; + +/** + * @author Nguyen Ky Thanh + */ +public interface AsyncTargeter { + + T target(FeignClientFactoryBean factory, AsyncBuilder feign, + FeignContext context, Target.HardCodedTarget target); + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/DefaultAsyncTargeter.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/DefaultAsyncTargeter.java new file mode 100644 index 000000000..f3b431f5d --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/DefaultAsyncTargeter.java @@ -0,0 +1,36 @@ +/* + * 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.async; + +import feign.AsyncFeign.AsyncBuilder; +import feign.Target; + +import org.springframework.cloud.openfeign.FeignClientFactoryBean; +import org.springframework.cloud.openfeign.FeignContext; + +/** + * @author Nguyen Ky Thanh + */ +public class DefaultAsyncTargeter implements AsyncTargeter { + + @Override + public T target(FeignClientFactoryBean factory, AsyncBuilder feign, + FeignContext context, Target.HardCodedTarget target) { + return feign.target(target); + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java new file mode 100644 index 000000000..1d3ca1ace --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java @@ -0,0 +1,158 @@ +/* + * 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 java.lang.reflect.Field; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Collections; + +import feign.AsyncClient; +import feign.ReflectiveAsyncFeign; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.openfeign.async.AsyncFeignAutoConfiguration; +import org.springframework.cloud.openfeign.async.AsyncTargeter; +import org.springframework.cloud.openfeign.async.DefaultAsyncTargeter; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.bind.annotation.RequestMapping; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.web.bind.annotation.RequestMethod.GET; + +/** + * @author Nguyen Ky Thanh + */ +class AsyncFeignClientFactoryTests { + + @Test + void testChildContexts() { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); + parent.refresh(); + FeignContext context = new FeignContext(); + context.setApplicationContext(parent); + context.setConfigurations(Arrays.asList(getSpec("foo", FooConfig.class), + getSpec("bar", BarConfig.class))); + + Foo foo = context.getInstance("foo", Foo.class); + assertThat(foo).as("foo was null").isNotNull(); + + Bar bar = context.getInstance("bar", Bar.class); + assertThat(bar).as("bar was null").isNotNull(); + + Bar foobar = context.getInstance("foo", Bar.class); + assertThat(foobar).as("bar was not null").isNull(); + } + + @Test + void shouldRedirectToDelegateWhenUrlSet() { + new ApplicationContextRunner().withUserConfiguration(TestConfig.class) + .run(this::defaultClientUsed); + } + + @SuppressWarnings({ "unchecked", "ConstantConditions" }) + private void defaultClientUsed(AssertableApplicationContext context) + throws Exception { + Proxy target = context.getBean(FeignClientFactoryBean.class).getAsyncTarget(); + Object asyncInvocationHandler = ReflectionTestUtils.getField(target, "h"); + + Field field = asyncInvocationHandler.getClass().getDeclaredField("this$0"); + field.setAccessible(true); + ReflectiveAsyncFeign reflectiveAsyncFeign = (ReflectiveAsyncFeign) field + .get(asyncInvocationHandler); + Object client = ReflectionTestUtils.getField(reflectiveAsyncFeign, "client"); + assertThat(client).isInstanceOf(AsyncClient.Default.class); + } + + private FeignClientSpecification getSpec(String name, Class configClass) { + return new FeignClientSpecification(name, new Class[] { configClass }); + } + + interface TestType { + + @RequestMapping(value = "/", method = GET) + String hello(); + + } + + @Configuration + static class TestConfig { + + @Bean + FeignContext feignContext() { + FeignContext feignContext = new FeignContext(); + feignContext.setConfigurations( + Collections.singletonList(new FeignClientSpecification("test", + new Class[] { AsyncFeignAutoConfiguration.class }))); + return feignContext; + } + + @Bean + FeignClientProperties feignClientProperties() { + return new FeignClientProperties(); + } + + @Bean + AsyncTargeter targeter() { + return new DefaultAsyncTargeter(); + } + + @Bean + FeignClientFactoryBean feignClientFactoryBean() { + FeignClientFactoryBean feignClientFactoryBean = new FeignClientFactoryBean(); + feignClientFactoryBean.setContextId("test"); + feignClientFactoryBean.setName("test"); + feignClientFactoryBean.setType(TestType.class); + feignClientFactoryBean.setPath(""); + feignClientFactoryBean.setUrl("http://some.absolute.url"); + return feignClientFactoryBean; + } + + } + + static class FooConfig { + + @Bean + Foo foo() { + return new Foo(); + } + + } + + static class Foo { + + } + + static class BarConfig { + + @Bean + Bar bar() { + return new Bar(); + } + + } + + static class Bar { + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java index b7ec76e42..2679f8db2 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java @@ -94,8 +94,8 @@ public void safetyCheckForNewFieldsOnTheFeignClientAnnotation() { // on this builder class. // (2) Or a new field was added and the builder class has to be extended with this // new field. - assertThat(methodNames).containsExactly("contextId", "decode404", "fallback", - "fallbackFactory", "name", "path", "url"); + assertThat(methodNames).containsExactly("asynchronous", "contextId", "decode404", + "fallback", "fallbackFactory", "name", "path", "url"); } @Test From efa7efd4d133dfdf9fda6c1448047b0a637efcaf Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Sat, 13 Mar 2021 12:35:50 +0100 Subject: [PATCH 2/6] New async flow [Done], first version. --- .../openfeign/FeignClientsConfiguration.java | 11 +++- .../async/AsyncFeignAutoConfiguration.java | 25 +++------- .../main/resources/META-INF/spring.factories | 3 +- .../AsyncFeignAutoConfigurationTests.java | 50 +++++++++++++++++++ 4 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index 400b59002..baf5482a4 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -20,6 +20,7 @@ import java.util.List; import com.netflix.hystrix.HystrixCommand; +import feign.AsyncFeign; import feign.Contract; import feign.Feign; import feign.Logger; @@ -35,6 +36,7 @@ import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -158,12 +160,19 @@ public Retryer feignRetryer() { } @Bean - @Scope("prototype") + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @ConditionalOnMissingBean public Feign.Builder feignBuilder(Retryer retryer) { return Feign.builder().retryer(retryer); } + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + @ConditionalOnMissingBean + public AsyncFeign.AsyncBuilder asyncFeignBuilder() { + return AsyncFeign.asyncBuilder(); + } + @Bean @ConditionalOnMissingBean(FeignLoggerFactory.class) public FeignLoggerFactory feignLoggerFactory() { diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java index fe716df12..067723f54 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java @@ -16,35 +16,24 @@ package org.springframework.cloud.openfeign.async; +import feign.AsyncClient; import feign.AsyncFeign; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.openfeign.FeignClientProperties; -import org.springframework.cloud.openfeign.FeignClientsConfiguration; -import org.springframework.cloud.openfeign.support.FeignEncoderProperties; -import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Scope; +/** + * An autoconfiguration that instantiates implementations of {@link AsyncClient} and + * implementations of {@link AsyncTargeter}. + * + * @author Nguyen Ky Thanh + */ @ConditionalOnClass(AsyncFeign.class) @Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties({ FeignClientProperties.class, - FeignHttpClientProperties.class, FeignEncoderProperties.class }) -@Import(FeignClientsConfiguration.class) public class AsyncFeignAutoConfiguration { - @Bean - @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) - @ConditionalOnMissingBean - public AsyncFeign.AsyncBuilder asyncFeignBuilder() { - return AsyncFeign.asyncBuilder(); - } - @Configuration(proxyBeanMethods = false) protected static class DefaultAsyncFeignTargeterConfiguration { diff --git a/spring-cloud-openfeign-core/src/main/resources/META-INF/spring.factories b/spring-cloud-openfeign-core/src/main/resources/META-INF/spring.factories index cd46e46c1..b0e676bc6 100644 --- a/spring-cloud-openfeign-core/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-openfeign-core/src/main/resources/META-INF/spring.factories @@ -4,4 +4,5 @@ org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,\ org.springframework.cloud.openfeign.FeignAutoConfiguration,\ org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\ org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,\ -org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration +org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration, \ +org.springframework.cloud.openfeign.async.AsyncFeignAutoConfiguration diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java new file mode 100644 index 000000000..033781377 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java @@ -0,0 +1,50 @@ +/* + * 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.async; + +import java.util.Map; + +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 Nguyen Ky Thanh + */ +public class AsyncFeignAutoConfigurationTests { + @Test + void shouldInstantiateDefaultAsyncTargeter() { + ConfigurableApplicationContext context = initContext(); + assertThatOneBeanPresent(context, DefaultAsyncTargeter.class); + } + + private ConfigurableApplicationContext initContext(String... properties) { + return new SpringApplicationBuilder().web(WebApplicationType.NONE) + .properties(properties) + .sources(AsyncFeignAutoConfiguration.class) + .run(); + } + + private void assertThatOneBeanPresent(ConfigurableApplicationContext context, + Class beanClass) { + Map beans = context.getBeansOfType(beanClass); + assertThat(beans).hasSize(1); + } +} From 80640e626cd7025425788e60b2dfdfe11c2b74b1 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Sun, 14 Mar 2021 00:14:43 +0100 Subject: [PATCH 3/6] Fix checkstyle --- .../async/AsyncFeignAutoConfigurationTests.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java index 033781377..42a3f04e5 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.cloud.openfeign.async; import java.util.Map; @@ -29,6 +30,7 @@ * @author Nguyen Ky Thanh */ public class AsyncFeignAutoConfigurationTests { + @Test void shouldInstantiateDefaultAsyncTargeter() { ConfigurableApplicationContext context = initContext(); @@ -37,14 +39,13 @@ void shouldInstantiateDefaultAsyncTargeter() { private ConfigurableApplicationContext initContext(String... properties) { return new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties(properties) - .sources(AsyncFeignAutoConfiguration.class) - .run(); + .properties(properties).sources(AsyncFeignAutoConfiguration.class).run(); } private void assertThatOneBeanPresent(ConfigurableApplicationContext context, - Class beanClass) { + Class beanClass) { Map beans = context.getBeansOfType(beanClass); assertThat(beans).hasSize(1); } + } From d268012ee6348211c858fd1ea1146a5e1c46e43c Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Sun, 14 Mar 2021 17:16:26 +0100 Subject: [PATCH 4/6] Add Apache HC5 Async option. --- .../async/AsyncFeignAutoConfiguration.java | 20 ++ .../AsyncHttpClient5FeignConfiguration.java | 175 +++++++++++++++++ .../support/FeignHttpClientProperties.java | 178 +++++++++++++++--- ...itional-spring-configuration-metadata.json | 6 + .../AsyncFeignClientFactoryTests.java | 40 ++-- .../AsyncFeignAutoConfigurationTests.java | 22 ++- ...syncHttpClient5FeignConfigurationTest.java | 93 +++++++++ .../FeignHttpClientPropertiesTests.java | 31 ++- 8 files changed, 526 insertions(+), 39 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncHttpClient5FeignConfiguration.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncHttpClient5FeignConfigurationTest.java diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java index 067723f54..11fb100bf 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfiguration.java @@ -18,11 +18,16 @@ import feign.AsyncClient; import feign.AsyncFeign; +import feign.hc5.AsyncApacheHttp5Client; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.protocol.HttpClientContext; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; /** * An autoconfiguration that instantiates implementations of {@link AsyncClient} and @@ -34,6 +39,21 @@ @Configuration(proxyBeanMethods = false) public class AsyncFeignAutoConfiguration { + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(AsyncApacheHttp5Client.class) + @ConditionalOnProperty(value = "feign.httpclient.asyncHc5.enabled", + havingValue = "true") + @Import(org.springframework.cloud.openfeign.async.AsyncHttpClient5FeignConfiguration.class) + protected static class AsyncHttpClient5FeignConfiguration { + + @Bean + public AsyncClient asyncClient( + CloseableHttpAsyncClient httpAsyncClient) { + return new AsyncApacheHttp5Client(httpAsyncClient); + } + + } + @Configuration(proxyBeanMethods = false) protected static class DefaultAsyncFeignTargeterConfiguration { diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncHttpClient5FeignConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncHttpClient5FeignConfiguration.java new file mode 100644 index 000000000..e9eb36505 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/async/AsyncHttpClient5FeignConfiguration.java @@ -0,0 +1,175 @@ +/* + * 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.async; + +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.cookie.StandardCookieSpec; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.http.ssl.TLS; +import org.apache.hc.core5.http2.HttpVersionPolicy; +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.reactor.IOReactorConfig; +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.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Nguyen Ky Thanh + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(CloseableHttpAsyncClient.class) +@EnableConfigurationProperties(FeignHttpClientProperties.class) +public class AsyncHttpClient5FeignConfiguration { + + private static final Log LOG = LogFactory + .getLog(AsyncHttpClient5FeignConfiguration.class); + + private CloseableHttpAsyncClient asyncHttpClient5; + + @Bean + @ConditionalOnMissingBean(HttpClientConnectionManager.class) + public AsyncClientConnectionManager asyncHc5ConnectionManager( + FeignHttpClientProperties httpClientProperties) { + return PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy( + clientTlsStrategy(httpClientProperties.isDisableSslValidation())) + .setMaxConnTotal(httpClientProperties.getMaxConnections()) + .setMaxConnPerRoute(httpClientProperties.getMaxConnectionsPerRoute()) + .setConnPoolPolicy(PoolReusePolicy.valueOf( + httpClientProperties.getAsyncHc5().getPoolReusePolicy().name())) + .setPoolConcurrencyPolicy( + PoolConcurrencyPolicy.valueOf(httpClientProperties.getAsyncHc5() + .getPoolConcurrencyPolicy().name())) + .setConnectionTimeToLive( + TimeValue.of(httpClientProperties.getTimeToLive(), + httpClientProperties.getTimeToLiveUnit())) + .build(); + } + + @Bean + public CloseableHttpAsyncClient asyncHttpClient5( + AsyncClientConnectionManager asyncClientConnectionManager, + FeignHttpClientProperties httpClientProperties) { + final IOReactorConfig ioReactorConfig = IOReactorConfig.custom() + .setSoTimeout(Timeout + .ofMilliseconds(httpClientProperties.getConnectionTimerRepeat())) + .build(); + asyncHttpClient5 = HttpAsyncClients.custom().disableCookieManagement() + .useSystemProperties() // Need for proxy + .setVersionPolicy(HttpVersionPolicy.valueOf( + httpClientProperties.getAsyncHc5().getHttpVersionPolicy().name())) // Need + // for + // proxy + .setConnectionManager(asyncClientConnectionManager) + .evictExpiredConnections().setIOReactorConfig(ioReactorConfig) + .setDefaultRequestConfig( + RequestConfig.custom() + .setConnectTimeout(Timeout.of( + httpClientProperties.getConnectionTimeout(), + TimeUnit.MILLISECONDS)) + .setResponseTimeout(Timeout.of( + httpClientProperties.getAsyncHc5() + .getResponseTimeout(), + httpClientProperties.getAsyncHc5() + .getResponseTimeoutUnit())) + .setCookieSpec(StandardCookieSpec.STRICT).build()) + .build(); + asyncHttpClient5.start(); + return asyncHttpClient5; + } + + @PreDestroy + public void destroy() { + if (asyncHttpClient5 != null) { + asyncHttpClient5.close(CloseMode.GRACEFUL); + } + } + + private TlsStrategy clientTlsStrategy(boolean isDisableSslValidation) { + final ClientTlsStrategyBuilder clientTlsStrategyBuilder = ClientTlsStrategyBuilder + .create(); + + if (isDisableSslValidation) { + try { + final SSLContext disabledSslContext = SSLContext.getInstance("SSL"); + disabledSslContext.init(null, new TrustManager[] { + new AsyncHttpClient5FeignConfiguration.DisabledValidationTrustManager() }, + new SecureRandom()); + clientTlsStrategyBuilder.setSslContext(disabledSslContext); + } + catch (NoSuchAlgorithmException e) { + LOG.warn("Error creating SSLContext", e); + } + catch (KeyManagementException e) { + LOG.warn("Error creating SSLContext", e); + } + } + else { + clientTlsStrategyBuilder.setSslContext(SSLContexts.createSystemDefault()); + } + + return clientTlsStrategyBuilder.setTlsVersions(TLS.V_1_3, TLS.V_1_2).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/support/FeignHttpClientProperties.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/FeignHttpClientProperties.java index 602caa4a4..f53d9a005 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 @@ -88,6 +88,11 @@ public class FeignHttpClientProperties { */ private Hc5Properties hc5 = new Hc5Properties(); + /** + * Apache Async HttpClient5 additional properties. + */ + private AsyncHc5Properties asyncHc5 = new AsyncHc5Properties(); + public int getConnectionTimerRepeat() { return this.connectionTimerRepeat; } @@ -160,6 +165,14 @@ public void setHc5(Hc5Properties hc5) { this.hc5 = hc5; } + public AsyncHc5Properties getAsyncHc5() { + return asyncHc5; + } + + public void setAsyncHc5(AsyncHc5Properties asyncHc5) { + this.asyncHc5 = asyncHc5; + } + public static class Hc5Properties { /** @@ -235,42 +248,161 @@ public void setSocketTimeout(int socketTimeout) { this.socketTimeout = socketTimeout; } + } + + public static class AsyncHc5Properties { + /** - * Enumeration of pool concurrency policies. + * Default value for pool concurrency policy. */ - public enum PoolConcurrencyPolicy { + public static final PoolConcurrencyPolicy DEFAULT_POOL_CONCURRENCY_POLICY = PoolConcurrencyPolicy.STRICT; - /** - * Higher concurrency but with lax connection max limit guarantees. - */ - LAX, + /** + * Default value for pool reuse policy. + */ + public static final PoolReusePolicy DEFAULT_POOL_REUSE_POLICY = PoolReusePolicy.FIFO; - /** - * Strict connection max limit guarantees. - */ - STRICT + /** + * Default value for response timeout. + */ + public static final int DEFAULT_RESPONSE_TIMEOUT = 0; - } + /** + * Default value for response timeout unit. + */ + public static final TimeUnit DEFAULT_RESPONSE_TIMEOUT_UNIT = TimeUnit.SECONDS; /** - * Enumeration of pooled connection re-use policies. + * Default HTTP protocol version policy. */ - public enum PoolReusePolicy { + private static final HttpVersionPolicy DEFAULT_HTTP_VERSION_POLICY = HttpVersionPolicy.FORCE_HTTP_1; - /** - * Re-use as few connections as possible making it possible for connections to - * become idle and expire. - */ - LIFO, + /** + * Pool concurrency policies. + */ + private PoolConcurrencyPolicy poolConcurrencyPolicy = DEFAULT_POOL_CONCURRENCY_POLICY; - /** - * Re-use all connections equally preventing them from becoming idle and - * expiring. - */ - FIFO + /** + * Pool connection re-use policies. + */ + private PoolReusePolicy poolReusePolicy = DEFAULT_POOL_REUSE_POLICY; + /** + * Determines the timeout until arrival of a response from the opposite endpoint. + * A timeout value of zero is interpreted as an infinite timeout. Please note that + * response timeout may be unsupported by HTTP transports with message + * multiplexing. + */ + private int responseTimeout = DEFAULT_RESPONSE_TIMEOUT; + + /** + * Default value for response timeout unit. + */ + private TimeUnit responseTimeoutUnit = DEFAULT_RESPONSE_TIMEOUT_UNIT; + + /** + * HTTP protocol version policy. + */ + private HttpVersionPolicy httpVersionPolicy = DEFAULT_HTTP_VERSION_POLICY; + + 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 int getResponseTimeout() { + return responseTimeout; + } + + public void setResponseTimeout(int responseTimeout) { + this.responseTimeout = responseTimeout; + } + + public TimeUnit getResponseTimeoutUnit() { + return responseTimeoutUnit; + } + + public void setResponseTimeoutUnit(TimeUnit responseTimeoutUnit) { + this.responseTimeoutUnit = responseTimeoutUnit; + } + + public HttpVersionPolicy getHttpVersionPolicy() { + return httpVersionPolicy; + } + + public void setHttpVersionPolicy(HttpVersionPolicy httpVersionPolicy) { + this.httpVersionPolicy = httpVersionPolicy; + } + + } + + /** + * HTTP protocol version policy. + */ + public enum HttpVersionPolicy { + + /** + * Force to use HTTP v1. + */ + FORCE_HTTP_1, + + /** + * Force to use HTTP v2. + */ + FORCE_HTTP_2, + + /** + * Try to use HTTP v2 otherwise fallback to HTTP v1. + */ + NEGOTIATE + + } + + /** + * 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 7ceb60088..91353dafd 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 @@ -38,6 +38,12 @@ "description": "Enables the use of the OK HTTP Client by Feign.", "defaultValue": "false" }, + { + "name": "feign.httpclient.asyncHc5.enabled", + "type": "java.lang.Boolean", + "description": "Enables the use of the Apache Async HTTP Client 5 by Feign for all @FeignClient with asynchronous=true.", + "defaultValue": "false" + }, { "name": "feign.compression.response.enabled", "type": "java.lang.Boolean", diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java index 1d3ca1ace..9e7d972f6 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java @@ -23,16 +23,19 @@ import feign.AsyncClient; import feign.ReflectiveAsyncFeign; +import feign.hc5.AsyncApacheHttp5Client; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.protocol.HttpClientContext; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.openfeign.async.AsyncFeignAutoConfiguration; -import org.springframework.cloud.openfeign.async.AsyncTargeter; -import org.springframework.cloud.openfeign.async.DefaultAsyncTargeter; +import org.springframework.cloud.openfeign.async.AsyncHttpClient5FeignConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.bind.annotation.RequestMapping; @@ -64,13 +67,19 @@ void testChildContexts() { } @Test - void shouldRedirectToDelegateWhenUrlSet() { + void asyncHc5ClientShouldBeUsed() { new ApplicationContextRunner().withUserConfiguration(TestConfig.class) - .run(this::defaultClientUsed); + .withUserConfiguration(AsyncHc5Config.class) + .run(context -> checkClientUsed(context, AsyncApacheHttp5Client.class)); } - @SuppressWarnings({ "unchecked", "ConstantConditions" }) - private void defaultClientUsed(AssertableApplicationContext context) + @Test + void defaultClientShouldBeUsed() { + new ApplicationContextRunner().withUserConfiguration(TestConfig.class) + .run(context -> checkClientUsed(context, AsyncClient.Default.class)); + } + + private void checkClientUsed(AssertableApplicationContext context, Class clientClass) throws Exception { Proxy target = context.getBean(FeignClientFactoryBean.class).getAsyncTarget(); Object asyncInvocationHandler = ReflectionTestUtils.getField(target, "h"); @@ -80,7 +89,7 @@ private void defaultClientUsed(AssertableApplicationContext context) ReflectiveAsyncFeign reflectiveAsyncFeign = (ReflectiveAsyncFeign) field .get(asyncInvocationHandler); Object client = ReflectionTestUtils.getField(reflectiveAsyncFeign, "client"); - assertThat(client).isInstanceOf(AsyncClient.Default.class); + assertThat(client).isInstanceOf(clientClass); } private FeignClientSpecification getSpec(String name, Class configClass) { @@ -94,6 +103,18 @@ interface TestType { } + @Configuration + @Import(AsyncHttpClient5FeignConfiguration.class) + static class AsyncHc5Config { + + @Bean + public AsyncClient asyncClient( + CloseableHttpAsyncClient httpAsyncClient) { + return new AsyncApacheHttp5Client(httpAsyncClient); + } + + } + @Configuration static class TestConfig { @@ -111,11 +132,6 @@ FeignClientProperties feignClientProperties() { return new FeignClientProperties(); } - @Bean - AsyncTargeter targeter() { - return new DefaultAsyncTargeter(); - } - @Bean FeignClientFactoryBean feignClientFactoryBean() { FeignClientFactoryBean feignClientFactoryBean = new FeignClientFactoryBean(); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java index 42a3f04e5..e2b6e02c4 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncFeignAutoConfigurationTests.java @@ -18,6 +18,10 @@ import java.util.Map; +import feign.AsyncClient; +import feign.hc5.AsyncApacheHttp5Client; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; import org.junit.jupiter.api.Test; import org.springframework.boot.WebApplicationType; @@ -32,9 +36,19 @@ public class AsyncFeignAutoConfigurationTests { @Test - void shouldInstantiateDefaultAsyncTargeter() { + void shouldInstantiateAsyncHc5FeignWhenAsyncHc5Enabled() { + ConfigurableApplicationContext context = initContext( + "feign.httpclient.asyncHc5.enabled=true"); + assertThatOneBeanPresent(context, AsyncApacheHttp5Client.class); + assertThatOneBeanPresent(context, CloseableHttpAsyncClient.class); + assertThatOneBeanPresent(context, AsyncClientConnectionManager.class); + } + + @Test + void shouldInstantiateDefaultAsyncTargeterAndNoAnyAsyncClient() { ConfigurableApplicationContext context = initContext(); assertThatOneBeanPresent(context, DefaultAsyncTargeter.class); + assertThatBeanNotPresent(context, AsyncClient.class); } private ConfigurableApplicationContext initContext(String... properties) { @@ -48,4 +62,10 @@ private void assertThatOneBeanPresent(ConfigurableApplicationContext context, assertThat(beans).hasSize(1); } + private void assertThatBeanNotPresent(ConfigurableApplicationContext context, + Class beanClass) { + Map beans = context.getBeansOfType(beanClass); + assertThat(beans).isEmpty(); + } + } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncHttpClient5FeignConfigurationTest.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncHttpClient5FeignConfigurationTest.java new file mode 100644 index 000000000..e7ed8942e --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/async/AsyncHttpClient5FeignConfigurationTest.java @@ -0,0 +1,93 @@ +/* + * 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.async; + +import java.lang.reflect.Field; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLContextSpi; +import javax.net.ssl.X509TrustManager; + +import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; +import org.apache.hc.core5.http.config.Lookup; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +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.cloud.commons.httpclient.HttpClientConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Nguyen Ky Thanh + */ +class AsyncHttpClient5FeignConfigurationTest { + + private ConfigurableApplicationContext context; + + @BeforeEach + void setUp() { + context = new SpringApplicationBuilder() + .properties("feign.httpclient.disableSslValidation=true", + "feign.httpclient.asyncHc5.enabled=true") + .web(WebApplicationType.NONE) + .sources(HttpClientConfiguration.class, AsyncFeignAutoConfiguration.class) + .run(); + } + + @AfterEach + void tearDown() { + if (context != null) { + context.close(); + } + } + + @Test + void disableSslTest() { + AsyncClientConnectionManager connectionManager = context + .getBean(AsyncClientConnectionManager.class); + Lookup tlsStrategyLookup = getTlsStrategyLookup(connectionManager); + assertThat(tlsStrategyLookup.lookup("https")).isNotNull(); + assertThat(getX509TrustManager(tlsStrategyLookup).getAcceptedIssuers()).isNull(); + } + + private Lookup getTlsStrategyLookup( + AsyncClientConnectionManager connectionManager) { + Object connectionOperator = getField(connectionManager, "connectionOperator"); + return (Lookup) getField(connectionOperator, "tlsStrategyLookup"); + } + + private X509TrustManager getX509TrustManager(Lookup tlsStrategyLookup) { + TlsStrategy tlsStrategy = tlsStrategyLookup.lookup("https"); + SSLContext sslContext = (SSLContext) getField(tlsStrategy, "sslContext"); + SSLContextSpi sslContextSpi = (SSLContextSpi) getField(sslContext, "contextSpi"); + return (X509TrustManager) getField(sslContextSpi, "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; + } + +} 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 d6fca9191..6c2a1a2d9 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 @@ -25,8 +25,8 @@ 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.cloud.openfeign.support.FeignHttpClientProperties.PoolConcurrencyPolicy; +import org.springframework.cloud.openfeign.support.FeignHttpClientProperties.PoolReusePolicy; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -34,6 +34,8 @@ import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.openfeign.support.FeignHttpClientProperties.AsyncHc5Properties.DEFAULT_RESPONSE_TIMEOUT; +import static org.springframework.cloud.openfeign.support.FeignHttpClientProperties.AsyncHc5Properties.DEFAULT_RESPONSE_TIMEOUT_UNIT; 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; @@ -69,6 +71,7 @@ public void testDefaults() { .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()) @@ -77,6 +80,15 @@ public void testDefaults() { .isEqualTo(DEFAULT_SOCKET_TIMEOUT); assertThat(getProperties().getHc5().getSocketTimeoutUnit()) .isEqualTo(DEFAULT_SOCKET_TIMEOUT_UNIT); + + assertThat(getProperties().getAsyncHc5().getPoolConcurrencyPolicy()) + .isEqualTo(PoolConcurrencyPolicy.STRICT); + assertThat(getProperties().getAsyncHc5().getPoolReusePolicy()) + .isEqualTo(PoolReusePolicy.FIFO); + assertThat(getProperties().getAsyncHc5().getResponseTimeout()) + .isEqualTo(DEFAULT_RESPONSE_TIMEOUT); + assertThat(getProperties().getAsyncHc5().getResponseTimeoutUnit()) + .isEqualTo(DEFAULT_RESPONSE_TIMEOUT_UNIT); } @Test @@ -93,7 +105,11 @@ public void testCustomization() { "feign.httpclient.hc5.poolConcurrencyPolicy=lax", "feign.httpclient.hc5.poolReusePolicy=lifo", "feign.httpclient.hc5.socketTimeout=200", - "feign.httpclient.hc5.socketTimeoutUnit=milliseconds") + "feign.httpclient.hc5.socketTimeoutUnit=milliseconds", + "feign.httpclient.asyncHc5.poolConcurrencyPolicy=lax", + "feign.httpclient.asyncHc5.poolReusePolicy=lifo", + "feign.httpclient.asyncHc5.responseTimeout=60", + "feign.httpclient.asyncHc5.responseTimeoutUnit=seconds") .applyTo(this.context); setupContext(); assertThat(getProperties().getMaxConnections()).isEqualTo(2); @@ -102,6 +118,7 @@ public void testCustomization() { assertThat(getProperties().getTimeToLive()).isEqualTo(2L); assertThat(getProperties().isDisableSslValidation()).isTrue(); assertThat(getProperties().isFollowRedirects()).isFalse(); + assertThat(getProperties().getHc5().getPoolConcurrencyPolicy()) .isEqualTo(PoolConcurrencyPolicy.LAX); assertThat(getProperties().getHc5().getPoolReusePolicy()) @@ -109,6 +126,14 @@ public void testCustomization() { assertThat(getProperties().getHc5().getSocketTimeout()).isEqualTo(200); assertThat(getProperties().getHc5().getSocketTimeoutUnit()) .isEqualTo(TimeUnit.MILLISECONDS); + + assertThat(getProperties().getAsyncHc5().getPoolConcurrencyPolicy()) + .isEqualTo(PoolConcurrencyPolicy.LAX); + assertThat(getProperties().getAsyncHc5().getPoolReusePolicy()) + .isEqualTo(PoolReusePolicy.LIFO); + assertThat(getProperties().getAsyncHc5().getResponseTimeout()).isEqualTo(60); + assertThat(getProperties().getAsyncHc5().getResponseTimeoutUnit()) + .isEqualTo(TimeUnit.SECONDS); } private void setupContext() { From b6a45251b7148b43d5313dba5683d4fd6e9424e0 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 15 Mar 2021 00:22:21 +0100 Subject: [PATCH 5/6] Add more coverage --- .../AsyncFeignClientFactoryTests.java | 8 +- .../FeignClientOverrideDefaultsTests.java | 131 +++++++++++++++++- 2 files changed, 131 insertions(+), 8 deletions(-) diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java index 9e7d972f6..27104a5f8 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/AsyncFeignClientFactoryTests.java @@ -69,7 +69,8 @@ void testChildContexts() { @Test void asyncHc5ClientShouldBeUsed() { new ApplicationContextRunner().withUserConfiguration(TestConfig.class) - .withUserConfiguration(AsyncHc5Config.class) + .withUserConfiguration(AsyncHc5Config.class, + FeignClientsConfiguration.class) .run(context -> checkClientUsed(context, AsyncApacheHttp5Client.class)); } @@ -81,7 +82,9 @@ void defaultClientShouldBeUsed() { private void checkClientUsed(AssertableApplicationContext context, Class clientClass) throws Exception { - Proxy target = context.getBean(FeignClientFactoryBean.class).getAsyncTarget(); + Object targetObject = context.getBean(FeignClientFactoryBean.class).getObject(); + assertThat(targetObject).isNotNull(); + Proxy target = (Proxy) targetObject; Object asyncInvocationHandler = ReflectionTestUtils.getField(target, "h"); Field field = asyncInvocationHandler.getClass().getDeclaredField("this$0"); @@ -140,6 +143,7 @@ FeignClientFactoryBean feignClientFactoryBean() { feignClientFactoryBean.setType(TestType.class); feignClientFactoryBean.setPath(""); feignClientFactoryBean.setUrl("http://some.absolute.url"); + feignClientFactoryBean.setAsynchronous(true); return feignClientFactoryBean; } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java index d8ff92ac0..2b1ba68d8 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java @@ -18,6 +18,7 @@ import java.util.concurrent.TimeUnit; +import feign.AsyncFeign.AsyncBuilder; import feign.Contract; import feign.ExceptionPropagationPolicy; import feign.Feign; @@ -41,6 +42,7 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration; +import org.springframework.cloud.openfeign.async.AsyncFeignAutoConfiguration; import org.springframework.cloud.openfeign.support.PageableSpringEncoder; import org.springframework.cloud.openfeign.support.SpringMvcContract; import org.springframework.context.annotation.Bean; @@ -54,6 +56,7 @@ /** * @author Spencer Gibb * @author Olga Maciaszek-Sharma + * @author Nguyen Ky Thanh */ @SpringBootTest(classes = FeignClientOverrideDefaultsTests.TestConfiguration.class) @DirtiesContext @@ -68,34 +71,50 @@ class FeignClientOverrideDefaultsTests { @Autowired private BarClient bar; + @Autowired + private FooAsyncClient fooAsync; + + @Autowired + private BarAsyncClient barAsync; + @Test void clientsAvailable() { assertThat(foo).isNotNull(); assertThat(bar).isNotNull(); + assertThat(fooAsync).isNotNull(); + assertThat(barAsync).isNotNull(); } @Test void overrideDecoder() { Decoder.Default.class.cast(context.getInstance("foo", Decoder.class)); OptionalDecoder.class.cast(context.getInstance("bar", Decoder.class)); + Decoder.Default.class.cast(context.getInstance("fooAsync", Decoder.class)); + OptionalDecoder.class.cast(context.getInstance("barAsync", Decoder.class)); } @Test void overrideEncoder() { Encoder.Default.class.cast(context.getInstance("foo", Encoder.class)); PageableSpringEncoder.class.cast(context.getInstance("bar", Encoder.class)); + Encoder.Default.class.cast(context.getInstance("fooAsync", Encoder.class)); + PageableSpringEncoder.class.cast(context.getInstance("barAsync", Encoder.class)); } @Test void overrideLogger() { Logger.JavaLogger.class.cast(context.getInstance("foo", Logger.class)); Slf4jLogger.class.cast(context.getInstance("bar", Logger.class)); + Logger.JavaLogger.class.cast(context.getInstance("fooAsync", Logger.class)); + Slf4jLogger.class.cast(context.getInstance("barAsync", Logger.class)); } @Test void overrideContract() { Contract.Default.class.cast(context.getInstance("foo", Contract.class)); SpringMvcContract.class.cast(context.getInstance("bar", Contract.class)); + Contract.Default.class.cast(context.getInstance("fooAsync", Contract.class)); + SpringMvcContract.class.cast(context.getInstance("barAsync", Contract.class)); } @Test @@ -103,6 +122,9 @@ void overrideLoggerLevel() { assertThat(context.getInstance("foo", Logger.Level.class)).isNull(); assertThat(context.getInstance("bar", Logger.Level.class)) .isEqualTo(Logger.Level.HEADERS); + assertThat(context.getInstance("fooAsync", Logger.Level.class)).isNull(); + assertThat(context.getInstance("barAsync", Logger.Level.class)) + .isEqualTo(Logger.Level.HEADERS); } @Test @@ -116,21 +138,32 @@ void overrideRetryer() { void overrideErrorDecoder() { assertThat(context.getInstance("foo", ErrorDecoder.class)).isNull(); ErrorDecoder.Default.class.cast(context.getInstance("bar", ErrorDecoder.class)); + assertThat(context.getInstance("fooAsync", ErrorDecoder.class)).isNull(); + ErrorDecoder.Default.class + .cast(context.getInstance("barAsync", ErrorDecoder.class)); } @Test void overrideBuilder() { HystrixFeign.Builder.class.cast(context.getInstance("foo", Feign.Builder.class)); Feign.Builder.class.cast(context.getInstance("bar", Feign.Builder.class)); + AsyncBuilder.class.cast(context.getInstance("fooAsync", AsyncBuilder.class)); + AsyncBuilder.class.cast(context.getInstance("barAsync", AsyncBuilder.class)); } @Test void overrideRequestOptions() { assertThat(context.getInstance("foo", Request.Options.class)).isNull(); - Request.Options options = context.getInstance("bar", Request.Options.class); - assertThat(options.connectTimeoutMillis()).isEqualTo(1); - assertThat(options.readTimeoutMillis()).isEqualTo(1); - assertThat(options.isFollowRedirects()).isFalse(); + assertThat(context.getInstance("fooAsync", Request.Options.class)).isNull(); + Request.Options barOptions = context.getInstance("bar", Request.Options.class); + assertThat(barOptions.connectTimeoutMillis()).isEqualTo(1); + assertThat(barOptions.readTimeoutMillis()).isEqualTo(1); + assertThat(barOptions.isFollowRedirects()).isFalse(); + Request.Options barAsyncOptions = context.getInstance("barAsync", + Request.Options.class); + assertThat(barAsyncOptions.connectTimeoutMillis()).isEqualTo(1); + assertThat(barAsyncOptions.readTimeoutMillis()).isEqualTo(1); + assertThat(barAsyncOptions.isFollowRedirects()).isFalse(); } @Test @@ -138,6 +171,10 @@ void overrideQueryMapEncoder() { QueryMapEncoder.Default.class .cast(context.getInstance("foo", QueryMapEncoder.class)); BeanQueryMapEncoder.class.cast(context.getInstance("bar", QueryMapEncoder.class)); + QueryMapEncoder.Default.class + .cast(context.getInstance("fooAsync", QueryMapEncoder.class)); + BeanQueryMapEncoder.class + .cast(context.getInstance("barAsync", QueryMapEncoder.class)); } @Test @@ -146,6 +183,10 @@ void addRequestInterceptor() { .isEqualTo(1); assertThat(context.getInstances("bar", RequestInterceptor.class).size()) .isEqualTo(2); + assertThat(context.getInstances("fooAsync", RequestInterceptor.class).size()) + .isEqualTo(1); + assertThat(context.getInstances("barAsync", RequestInterceptor.class).size()) + .isEqualTo(2); } @Test @@ -174,10 +215,29 @@ interface BarClient { } + @FeignClient(name = "fooAsync", url = "https://fooAsync", + configuration = FooAsyncConfiguration.class, asynchronous = true) + interface FooAsyncClient { + + @RequestLine("GET /") + String get(); + + } + + @FeignClient(name = "barAsync", url = "https://barAsync", + configuration = BarAsyncConfiguration.class, asynchronous = true) + interface BarAsyncClient { + + @GetMapping("/") + String get(); + + } + @Configuration(proxyBeanMethods = false) - @EnableFeignClients(clients = { FooClient.class, BarClient.class }) + @EnableFeignClients(clients = { FooClient.class, BarClient.class, + FooAsyncClient.class, BarAsyncClient.class }) @Import({ PropertyPlaceholderAutoConfiguration.class, ArchaiusAutoConfiguration.class, - FeignAutoConfiguration.class }) + FeignAutoConfiguration.class, AsyncFeignAutoConfiguration.class }) protected static class TestConfiguration { @Bean @@ -262,4 +322,63 @@ public ExceptionPropagationPolicy exceptionPropagationPolicy() { } + public static class FooAsyncConfiguration { + + @Bean + public Decoder feignDecoder() { + return new Decoder.Default(); + } + + @Bean + public Encoder feignEncoder() { + return new Encoder.Default(); + } + + @Bean + public Logger feignLogger() { + return new Logger.JavaLogger(); + } + + @Bean + public Contract feignContract() { + return new Contract.Default(); + } + + @Bean + public QueryMapEncoder queryMapEncoder() { + return new feign.QueryMapEncoder.Default(); + } + + } + + public static class BarAsyncConfiguration { + + @Bean + Logger.Level feignLevel() { + return Logger.Level.HEADERS; + } + + @Bean + ErrorDecoder feignErrorDecoder() { + return new ErrorDecoder.Default(); + } + + @Bean + Request.Options feignRequestOptions() { + return new Request.Options(1, TimeUnit.MILLISECONDS, 1, TimeUnit.MILLISECONDS, + false); + } + + @Bean + RequestInterceptor feignRequestInterceptor() { + return new BasicAuthRequestInterceptor("user", "pass"); + } + + @Bean + public QueryMapEncoder queryMapEncoder() { + return new BeanQueryMapEncoder(); + } + + } + } From d58559780916956261b1b508395155e7ba8b6c42 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 15 Mar 2021 01:31:23 +0100 Subject: [PATCH 6/6] More tests for async flow. --- .../FeignClientOverrideDefaultsTests.java | 2 +- .../FeignClientUsingPropertiesTests.java | 192 ++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java index 2b1ba68d8..d336a7f9e 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java @@ -224,7 +224,7 @@ interface FooAsyncClient { } - @FeignClient(name = "barAsync", url = "https://barAsync", + @FeignClient(name = "barAsync", url = "https://barAsync", decode404 = true, configuration = BarAsyncConfiguration.class, asynchronous = true) interface BarAsyncClient { diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingPropertiesTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingPropertiesTests.java index 300d54f14..e1e3d7cd7 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingPropertiesTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingPropertiesTests.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -76,6 +77,7 @@ * @author Eko Kurniawan Khannedy * @author Olga Maciaszek-Sharma * @author Ilia Ilinykh + * @author Nguyen Ky Thanh */ @SuppressWarnings("FieldMayBeFinal") @RunWith(SpringJUnit4ClassRunner.class) @@ -142,42 +144,84 @@ public FooClient fooClient() { "http://localhost:" + port); } + public FooAsyncClient fooAsyncClient() { + fooFactoryBean.setApplicationContext(applicationContext); + return (FooAsyncClient) fooFactoryBean.asyncFeign(context) + .target(FooAsyncClient.class, "http://localhost:" + port); + } + public BarClient barClient() { barFactoryBean.setApplicationContext(applicationContext); return barFactoryBean.feign(context).target(BarClient.class, "http://localhost:" + port); } + public BarAsyncClient barAsyncClient() { + barFactoryBean.setApplicationContext(applicationContext); + return (BarAsyncClient) barFactoryBean.asyncFeign(context) + .target(BarAsyncClient.class, "http://localhost:" + port); + } + public UnwrapClient unwrapClient() { unwrapFactoryBean.setApplicationContext(applicationContext); return unwrapFactoryBean.feign(context).target(UnwrapClient.class, "http://localhost:" + port); } + public UnwrapAsyncClient unwrapAsyncClient() { + unwrapFactoryBean.setApplicationContext(applicationContext); + return (UnwrapAsyncClient) unwrapFactoryBean.asyncFeign(context) + .target(UnwrapAsyncClient.class, "http://localhost:" + port); + } + public FormClient formClient() { formFactoryBean.setApplicationContext(applicationContext); return formFactoryBean.feign(context).target(FormClient.class, "http://localhost:" + port); } + public FormAsyncClient formAsyncClient() { + formFactoryBean.setApplicationContext(applicationContext); + return (FormAsyncClient) formFactoryBean.asyncFeign(context) + .target(FormAsyncClient.class, "http://localhost:" + port); + } + @Test public void testFoo() { String response = fooClient().foo(); assertThat(response).isEqualTo("OK"); } + @Test + public void testFooAsync() { + String response = fooAsyncClient().foo(); + assertThat(response).isEqualTo("OK"); + } + @Test(expected = RetryableException.class) public void testBar() { barClient().bar(); fail("it should timeout"); } + @Test(expected = CompletionException.class) + public void testBarAsync() { + barAsyncClient().bar(); + fail("it should timeout"); + } + @Test(expected = SocketTimeoutException.class) public void testUnwrap() throws Exception { unwrapClient().unwrap(); fail("it should timeout"); } + @Test(expected = CompletionException.class) + public void testUnwrapAsync() throws Exception { + unwrapAsyncClient().unwrap(); + fail("it should timeout"); + } + @Test public void testForm() { Map request = Collections.singletonMap("form", "Data"); @@ -185,12 +229,25 @@ public void testForm() { assertThat(response).isEqualTo("Data"); } + @Test + public void testAsyncForm() { + Map request = Collections.singletonMap("form", "Data"); + String response = formAsyncClient().form(request); + assertThat(response).isEqualTo("Data"); + } + @Test public void testSingleValue() { List response = singleValueClient().singleValue(); assertThat(response).isEqualTo(Arrays.asList("header", "parameter")); } + @Test + public void testSingleValueAsync() { + List response = singleValueAsyncClient().singleValue(); + assertThat(response).isEqualTo(Arrays.asList("header", "parameter")); + } + @Test public void testMultipleValue() { List response = multipleValueClient().multipleValue(); @@ -198,6 +255,13 @@ public void testMultipleValue() { Arrays.asList("header1", "header2", "parameter1", "parameter2")); } + @Test + public void testMultipleValueAsync() { + List response = multipleValueAsyncClient().multipleValue(); + assertThat(response).isEqualTo( + Arrays.asList("header1", "header2", "parameter1", "parameter2")); + } + public SingleValueClient singleValueClient() { this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean .setApplicationContext(this.applicationContext); @@ -206,6 +270,14 @@ public SingleValueClient singleValueClient() { .target(SingleValueClient.class, "http://localhost:" + this.port); } + public SingleValueAsyncClient singleValueAsyncClient() { + this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean + .setApplicationContext(this.applicationContext); + return (SingleValueAsyncClient) this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean + .asyncFeign(this.context) + .target(SingleValueAsyncClient.class, "http://localhost:" + this.port); + } + public MultipleValueClient multipleValueClient() { this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean .setApplicationContext(this.applicationContext); @@ -214,6 +286,14 @@ public MultipleValueClient multipleValueClient() { .target(MultipleValueClient.class, "http://localhost:" + this.port); } + public MultipleValueAsyncClient multipleValueAsyncClient() { + this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean + .setApplicationContext(this.applicationContext); + return (MultipleValueAsyncClient) this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean + .asyncFeign(this.context) + .target(MultipleValueAsyncClient.class, "http://localhost:" + this.port); + } + @Test public void readTimeoutShouldWorkWhenConnectTimeoutNotSet() { FeignClientFactoryBean readTimeoutFactoryBean = new FeignClientFactoryBean(); @@ -230,6 +310,23 @@ public void readTimeoutShouldWorkWhenConnectTimeoutNotSet() { assertThat(options.connectTimeoutMillis()).isEqualTo(5000); } + @Test + public void readTimeoutAsyncShouldWorkWhenConnectTimeoutNotSet() { + FeignClientFactoryBean readTimeoutFactoryBean = new FeignClientFactoryBean(); + readTimeoutFactoryBean.setContextId("readTimeout"); + readTimeoutFactoryBean.setType(FeignClientFactoryBean.class); + readTimeoutFactoryBean.setApplicationContext(applicationContext); + + TimeoutAsyncClient client = (TimeoutAsyncClient) readTimeoutFactoryBean + .asyncFeign(context) + .target(TimeoutAsyncClient.class, "http://localhost:" + port); + + Request.Options options = getAsyncRequestOptions((Proxy) client); + + assertThat(options.readTimeoutMillis()).isEqualTo(1000); + assertThat(options.connectTimeoutMillis()).isEqualTo(5000); + } + @Test public void connectTimeoutShouldWorkWhenReadTimeoutNotSet() { FeignClientFactoryBean readTimeoutFactoryBean = new FeignClientFactoryBean(); @@ -246,6 +343,23 @@ public void connectTimeoutShouldWorkWhenReadTimeoutNotSet() { assertThat(options.readTimeoutMillis()).isEqualTo(5000); } + @Test + public void connectTimeoutAsyncShouldWorkWhenReadTimeoutNotSet() { + FeignClientFactoryBean readTimeoutFactoryBean = new FeignClientFactoryBean(); + readTimeoutFactoryBean.setContextId("connectTimeout"); + readTimeoutFactoryBean.setType(FeignClientFactoryBean.class); + readTimeoutFactoryBean.setApplicationContext(applicationContext); + + TimeoutAsyncClient client = (TimeoutAsyncClient) readTimeoutFactoryBean + .asyncFeign(context) + .target(TimeoutAsyncClient.class, "http://localhost:" + port); + + Request.Options options = getAsyncRequestOptions((Proxy) client); + + assertThat(options.connectTimeoutMillis()).isEqualTo(1000); + assertThat(options.readTimeoutMillis()).isEqualTo(5000); + } + @Test public void shouldSetFollowRedirects() { FeignClientFactoryBean testFactoryBean = new FeignClientFactoryBean(); @@ -261,6 +375,22 @@ public void shouldSetFollowRedirects() { assertThat(options.isFollowRedirects()).isFalse(); } + @Test + public void shouldSetFollowRedirectsAsync() { + FeignClientFactoryBean testFactoryBean = new FeignClientFactoryBean(); + testFactoryBean.setContextId("test"); + testFactoryBean.setType(FeignClientFactoryBean.class); + testFactoryBean.setApplicationContext(applicationContext); + + TimeoutAsyncClient client = (TimeoutAsyncClient) testFactoryBean + .asyncFeign(context) + .target(TimeoutAsyncClient.class, "http://localhost:" + port); + + Request.Options options = getAsyncRequestOptions((Proxy) client); + + assertThat(options.isFollowRedirects()).isFalse(); + } + private Request.Options getRequestOptions(Proxy client) { Object invocationHandler = ReflectionTestUtils.getField(client, "h"); Map dispatch = (Map) ReflectionTestUtils @@ -270,6 +400,18 @@ private Request.Options getRequestOptions(Proxy client) { "options"); } + private Request.Options getAsyncRequestOptions(Proxy client) { + Object asyncInvocationHandler = ReflectionTestUtils.getField(client, "h"); + Object instance = ReflectionTestUtils.getField(asyncInvocationHandler, + "instance"); + Object invocationHandler = ReflectionTestUtils.getField(instance, "h"); + Map dispatch = (Map) ReflectionTestUtils + .getField(Objects.requireNonNull(invocationHandler), "dispatch"); + Method key = new ArrayList<>(dispatch.keySet()).get(0); + return (Request.Options) ReflectionTestUtils.getField(dispatch.get(key), + "options"); + } + protected interface FooClient { @GetMapping(path = "/foo") @@ -277,6 +419,13 @@ protected interface FooClient { } + protected interface FooAsyncClient { + + @GetMapping(path = "/foo") + String foo(); + + } + protected interface BarClient { @GetMapping(path = "/bar") @@ -284,6 +433,13 @@ protected interface BarClient { } + protected interface BarAsyncClient { + + @GetMapping(path = "/bar") + String bar(); + + } + protected interface UnwrapClient { @GetMapping(path = "/bar") // intentionally /bar @@ -291,6 +447,13 @@ protected interface UnwrapClient { } + protected interface UnwrapAsyncClient { + + @GetMapping(path = "/bar") // intentionally /bar + String unwrap() throws IOException; + + } + protected interface FormClient { @RequestMapping(value = "/form", method = RequestMethod.POST, @@ -299,6 +462,14 @@ protected interface FormClient { } + protected interface FormAsyncClient { + + @RequestMapping(value = "/form", method = RequestMethod.POST, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + String form(Map form); + + } + protected interface SingleValueClient { @GetMapping(path = "/singleValue") @@ -306,6 +477,13 @@ protected interface SingleValueClient { } + protected interface SingleValueAsyncClient { + + @GetMapping(path = "/singleValue") + List singleValue(); + + } + protected interface MultipleValueClient { @GetMapping(path = "/multipleValue") @@ -313,6 +491,13 @@ protected interface MultipleValueClient { } + protected interface MultipleValueAsyncClient { + + @GetMapping(path = "/multipleValue") + List multipleValue(); + + } + protected interface TimeoutClient { @GetMapping("/timeouts") @@ -320,6 +505,13 @@ protected interface TimeoutClient { } + protected interface TimeoutAsyncClient { + + @GetMapping("/timeouts") + String timeouts(); + + } + @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration @RestController