From 6996e8a35e2b7337f12664526167a1c531891cca Mon Sep 17 00:00:00 2001
From: Thanh Nguyen <thanh.nguyen-ky@klarna.com>
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<Object>, 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<String, FeignBuilderCustomizer> 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<String, RequestInterceptor> requestInterceptors = getInheritedAwareInstances(
+				context, RequestInterceptor.class);
+		if (requestInterceptors != null) {
+			List<RequestInterceptor> 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<RequestInterceptor> 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> T getOrInstantiate(Class<T> tClass) {
 		try {
 			return beanFactory != null ? beanFactory.getBean(tClass)
@@ -350,6 +512,9 @@ protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
 
 	@Override
 	public Object getObject() {
+		if (asynchronous) {
+			return getAsyncTarget();
+		}
 		return getTarget();
 	}
 
@@ -404,6 +569,33 @@ <T> T getTarget() {
 				new HardCodedTarget<>(type, name, url));
 	}
 
+	/**
+	 * @param <T> the target type of the Feign client
+	 * @return an {@link AsyncFeign} client created with the specified data and the
+	 * context information
+	 */
+	<T> 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<C> {
+
+	<T> T target(FeignClientFactoryBean factory, AsyncBuilder<C> feign,
+			FeignContext context, Target.HardCodedTarget<T> 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<C> implements AsyncTargeter<C> {
+
+	@Override
+	public <T> T target(FeignClientFactoryBean factory, AsyncBuilder<C> feign,
+			FeignContext context, Target.HardCodedTarget<T> 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 <thanh.nguyen-ky@klarna.com>
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<String, ?> beans = context.getBeansOfType(beanClass);
+		assertThat(beans).hasSize(1);
+	}
+}

From 80640e626cd7025425788e60b2dfdfe11c2b74b1 Mon Sep 17 00:00:00 2001
From: Thanh Nguyen <thanh.nguyen-ky@klarna.com>
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<String, ?> beans = context.getBeansOfType(beanClass);
 		assertThat(beans).hasSize(1);
 	}
+
 }

From d268012ee6348211c858fd1ea1146a5e1c46e43c Mon Sep 17 00:00:00 2001
From: Thanh Nguyen <thanh.nguyen-ky@klarna.com>
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<HttpClientContext> 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<HttpClientContext> 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<String, ?> 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<TlsStrategy> tlsStrategyLookup = getTlsStrategyLookup(connectionManager);
+		assertThat(tlsStrategyLookup.lookup("https")).isNotNull();
+		assertThat(getX509TrustManager(tlsStrategyLookup).getAcceptedIssuers()).isNull();
+	}
+
+	private Lookup<TlsStrategy> getTlsStrategyLookup(
+			AsyncClientConnectionManager connectionManager) {
+		Object connectionOperator = getField(connectionManager, "connectionOperator");
+		return (Lookup) getField(connectionOperator, "tlsStrategyLookup");
+	}
+
+	private X509TrustManager getX509TrustManager(Lookup<TlsStrategy> 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 <thanh.nguyen-ky@klarna.com>
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 <thanh.nguyen-ky@klarna.com>
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<String, String> request = Collections.singletonMap("form", "Data");
@@ -185,12 +229,25 @@ public void testForm() {
 		assertThat(response).isEqualTo("Data");
 	}
 
+	@Test
+	public void testAsyncForm() {
+		Map<String, String> request = Collections.singletonMap("form", "Data");
+		String response = formAsyncClient().form(request);
+		assertThat(response).isEqualTo("Data");
+	}
+
 	@Test
 	public void testSingleValue() {
 		List<String> response = singleValueClient().singleValue();
 		assertThat(response).isEqualTo(Arrays.asList("header", "parameter"));
 	}
 
+	@Test
+	public void testSingleValueAsync() {
+		List<String> response = singleValueAsyncClient().singleValue();
+		assertThat(response).isEqualTo(Arrays.asList("header", "parameter"));
+	}
+
 	@Test
 	public void testMultipleValue() {
 		List<String> response = multipleValueClient().multipleValue();
@@ -198,6 +255,13 @@ public void testMultipleValue() {
 				Arrays.asList("header1", "header2", "parameter1", "parameter2"));
 	}
 
+	@Test
+	public void testMultipleValueAsync() {
+		List<String> 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<Method, InvocationHandlerFactory.MethodHandler> dispatch = (Map<Method, InvocationHandlerFactory.MethodHandler>) 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<Method, InvocationHandlerFactory.MethodHandler> dispatch = (Map<Method, InvocationHandlerFactory.MethodHandler>) 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<String, String> form);
+
+	}
+
 	protected interface SingleValueClient {
 
 		@GetMapping(path = "/singleValue")
@@ -306,6 +477,13 @@ protected interface SingleValueClient {
 
 	}
 
+	protected interface SingleValueAsyncClient {
+
+		@GetMapping(path = "/singleValue")
+		List<String> singleValue();
+
+	}
+
 	protected interface MultipleValueClient {
 
 		@GetMapping(path = "/multipleValue")
@@ -313,6 +491,13 @@ protected interface MultipleValueClient {
 
 	}
 
+	protected interface MultipleValueAsyncClient {
+
+		@GetMapping(path = "/multipleValue")
+		List<String> 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