diff --git a/pom.xml b/pom.xml
index 5e56f687..fc26742b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -243,6 +243,11 @@
feign-slf4j
${feign.version}
+
+ com.netflix.feign
+ feign-httpclient
+ ${feign.version}
+
com.netflix.hystrix
hystrix-core
diff --git a/spring-cloud-netflix-core/pom.xml b/spring-cloud-netflix-core/pom.xml
index 86947fe0..a9d685ef 100644
--- a/spring-cloud-netflix-core/pom.xml
+++ b/spring-cloud-netflix-core/pom.xml
@@ -91,6 +91,11 @@
feign-slf4j
true
+
+ com.netflix.feign
+ feign-httpclient
+ true
+
com.netflix.feign
feign-ribbon
diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java
index 033cbed6..8ccba6ee 100644
--- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java
+++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java
@@ -16,8 +16,13 @@
package org.springframework.cloud.netflix.feign;
+import feign.Client;
+import feign.httpclient.ApacheHttpClient;
+import org.apache.http.client.HttpClient;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder;
import org.springframework.cloud.netflix.feign.support.SpringDecoder;
@@ -61,4 +66,19 @@ public class FeignClientsConfiguration {
return new SpringMvcContract();
}
+ @Configuration
+ @ConditionalOnClass(ApacheHttpClient.class)
+ protected static class HttpClientConfiguration {
+
+ @Autowired(required = false)
+ private HttpClient httpClient;
+
+ @ConditionalOnMissingBean
+ public Client feignClient() {
+ if (httpClient != null) {
+ return new ApacheHttpClient(httpClient);
+ }
+ return new ApacheHttpClient();
+ }
+ }
}
diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientAutoConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientAutoConfiguration.java
index 2889cf56..252b1205 100644
--- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientAutoConfiguration.java
+++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientAutoConfiguration.java
@@ -16,11 +16,15 @@
package org.springframework.cloud.netflix.feign.ribbon;
+import feign.httpclient.ApacheHttpClient;
+import feign.ribbon.LBClientFactory;
import feign.ribbon.RibbonClient;
+import org.apache.http.client.HttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.netflix.feign.FeignAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.Bean;
@@ -30,6 +34,7 @@ import com.netflix.loadbalancer.ILoadBalancer;
import feign.Client;
import feign.Feign;
+import org.springframework.context.annotation.Primary;
/**
* Autoconfiguration to be activated if Feign is in use and needs to be use Ribbon as a
@@ -51,13 +56,44 @@ public class FeignRibbonClientAutoConfiguration {
}
@Bean
+ @Primary
public CachingLBClientFactory cachingLBClientFactory() {
return new CachingLBClientFactory(springLBClientFactory());
}
@Bean
@ConditionalOnMissingBean
- public Client feignRibbonClient() {
+ public Client feignClient() {
return RibbonClient.builder().lbClientFactory(cachingLBClientFactory()).build();
}
+
+
+ @Configuration
+ @ConditionalOnClass(ApacheHttpClient.class)
+ @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
+ protected static class HttpClientConfiguration {
+
+ @Autowired(required = false)
+ private HttpClient httpClient;
+
+ @Autowired(required = false)
+ private LBClientFactory lbClientFactory;
+
+ @Bean
+ public Client feignClient() {
+ RibbonClient.Builder builder = RibbonClient.builder();
+
+ if (httpClient != null) {
+ builder.delegate(new ApacheHttpClient(httpClient));
+ } else {
+ builder.delegate(new ApacheHttpClient());
+ }
+
+ if (lbClientFactory != null) {
+ builder.lbClientFactory(lbClientFactory);
+ }
+
+ return builder.build();
+ }
+ }
}
\ No newline at end of file
diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/ResponseEntityDecoder.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/ResponseEntityDecoder.java
index bb99b2bc..b6a73935 100644
--- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/ResponseEntityDecoder.java
+++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/ResponseEntityDecoder.java
@@ -1,64 +1,61 @@
-
package org.springframework.cloud.netflix.feign.support;
-import feign.FeignException;
-import feign.Response;
-import feign.codec.Decoder;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.util.MultiValueMap;
-
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.LinkedList;
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import feign.FeignException;
+import feign.Response;
+import feign.codec.Decoder;
+
/**
- * Decoder adds compatibility for Spring MVC's ResponseEntity to any
- * other decoder via composition.
+ * Decoder adds compatibility for Spring MVC's ResponseEntity to any other decoder via
+ * composition.
* @author chadjaros
*/
@Slf4j
public class ResponseEntityDecoder implements Decoder {
- private Decoder decoder;
+ private Decoder decoder;
- public ResponseEntityDecoder(Decoder decoder) {
- this.decoder = decoder;
- }
+ public ResponseEntityDecoder(Decoder decoder) {
+ this.decoder = decoder;
+ }
- @Override
- public Object decode(final Response response, Type type) throws IOException,
- FeignException {
+ @Override
+ public Object decode(final Response response, Type type) throws IOException,
+ FeignException {
- if(type instanceof ParameterizedType &&
- ((ParameterizedType) type).getRawType().equals(ResponseEntity.class)) {
+ if (type instanceof ParameterizedType
+ && ((ParameterizedType) type).getRawType().equals(ResponseEntity.class)) {
- type = ((ParameterizedType) type).getActualTypeArguments()[0];
- Object decodedObject = decoder.decode(response, type);
+ type = ((ParameterizedType) type).getActualTypeArguments()[0];
+ Object decodedObject = decoder.decode(response, type);
- return createResponse(
- decodedObject.getClass(),
- decodedObject,
- response);
- }
- else {
- return decoder.decode(response, type);
- }
- }
+ return createResponse(decodedObject, response);
+ }
+ else {
+ return decoder.decode(response, type);
+ }
+ }
- private ResponseEntity createResponse(Class clazz, Object instance, Response response) {
+ @SuppressWarnings("unchecked")
+ private ResponseEntity createResponse(Object instance, Response response) {
- MultiValueMap headers = new LinkedMultiValueMap<>();
- for(String key: response.headers().keySet()) {
- headers.put(key, new LinkedList<>(response.headers().get(key)));
- }
+ MultiValueMap headers = new LinkedMultiValueMap<>();
+ for (String key : response.headers().keySet()) {
+ headers.put(key, new LinkedList<>(response.headers().get(key)));
+ }
- return new ResponseEntity(
- clazz.cast(instance),
- headers,
- HttpStatus.valueOf(response.status()));
- }
+ return new ResponseEntity<>((T) instance, headers, HttpStatus.valueOf(response
+ .status()));
+ }
}
\ No newline at end of file
diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java
index c7eb852a..253bee64 100644
--- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java
+++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java
@@ -16,6 +16,13 @@
package org.springframework.cloud.netflix.feign.valid;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
@@ -42,6 +49,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -51,11 +59,9 @@ import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
+import feign.Client;
import feign.RequestInterceptor;
import feign.RequestTemplate;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
/**
* @author Spencer Gibb
@@ -63,7 +69,8 @@ import static org.junit.Assert.assertTrue;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = FeignClientTests.Application.class)
@WebAppConfiguration
-@IntegrationTest({ "server.port=0", "spring.application.name=feignclienttest" })
+@IntegrationTest({ "server.port=0", "spring.application.name=feignclienttest",
+ "feign.httpclient.enabled=false"})
@DirtiesContext
public class FeignClientTests {
@@ -73,6 +80,9 @@ public class FeignClientTests {
@Autowired
private TestClient testClient;
+ @Autowired
+ private Client feignClient;
+
// @FeignClient(value = "http://localhost:9876", loadbalance = false)
@FeignClient("localapp")
protected static interface TestClient {
@@ -191,6 +201,15 @@ public class FeignClientTests {
headers.contains("myheader2value"));
}
+ @Test
+ public void testFeignClientType() throws IllegalAccessException {
+ assertThat(this.feignClient, is(instanceOf(feign.ribbon.RibbonClient.class)));
+ Field field = ReflectionUtils.findField(feign.ribbon.RibbonClient.class, "delegate", Client.class);
+ ReflectionUtils.makeAccessible(field);
+ Client delegate = (Client) field.get(this.feignClient);
+ assertThat(delegate, is(instanceOf(feign.Client.Default.class)));
+ }
+
@Data
@AllArgsConstructor
@NoArgsConstructor
diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignHttpClientTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignHttpClientTests.java
new file mode 100644
index 00000000..8034aafb
--- /dev/null
+++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignHttpClientTests.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2013-2015 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
+ *
+ * http://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.netflix.feign.valid;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.cloud.netflix.ribbon.RibbonClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.netflix.loadbalancer.BaseLoadBalancer;
+import com.netflix.loadbalancer.ILoadBalancer;
+import com.netflix.loadbalancer.Server;
+
+import feign.Client;
+
+/**
+ * @author Spencer Gibb
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringApplicationConfiguration(classes = FeignHttpClientTests.Application.class)
+@WebAppConfiguration
+@IntegrationTest({ "server.port=0", "spring.application.name=feignclienttest" })
+@DirtiesContext
+public class FeignHttpClientTests {
+
+ @Value("${local.server.port}")
+ private int port = 0;
+
+ @Autowired
+ private TestClient testClient;
+
+ @Autowired
+ private Client feignClient;
+
+ // @FeignClient(value = "http://localhost:9876", loadbalance = false)
+ @FeignClient("localapp")
+ protected static interface TestClient {
+ @RequestMapping(method = RequestMethod.GET, value = "/hello")
+ public Hello getHello();
+
+ @RequestMapping(method = RequestMethod.PATCH, value = "/hellop")
+ public ResponseEntity patchHello();
+ }
+
+ @Configuration
+ @EnableAutoConfiguration
+ @RestController
+ @EnableFeignClients
+ @RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class)
+ protected static class Application {
+
+ @RequestMapping(method = RequestMethod.GET, value = "/hello")
+ public Hello getHello() {
+ return new Hello("hello world 1");
+ }
+
+ @RequestMapping(method = RequestMethod.PATCH, value = "/hellop")
+ public ResponseEntity patchHello() {
+ return ResponseEntity.ok().header("X-Hello", "hello world patch").build();
+ }
+
+ public static void main(String[] args) {
+ new SpringApplicationBuilder(Application.class).properties(
+ "spring.application.name=feignclienttest",
+ "management.contextPath=/admin").run(args);
+ }
+ }
+
+ @Test
+ public void testSimpleType() {
+ Hello hello = this.testClient.getHello();
+ assertNotNull("hello was null", hello);
+ assertEquals("first hello didn't match", new Hello("hello world 1"), hello);
+ }
+
+ @Test
+ public void testPatch() {
+ ResponseEntity response = this.testClient.patchHello();
+ assertThat(response, is(notNullValue()));
+ String header = response.getHeaders().getFirst("X-Hello");
+ assertThat(header, equalTo("hello world patch"));
+ }
+
+ @Test
+ public void testFeignClientType() throws IllegalAccessException {
+ assertThat(this.feignClient, is(instanceOf(feign.ribbon.RibbonClient.class)));
+ Field field = ReflectionUtils.findField(feign.ribbon.RibbonClient.class,
+ "delegate", Client.class);
+ ReflectionUtils.makeAccessible(field);
+ Client delegate = (Client) field.get(this.feignClient);
+ assertThat(delegate, is(instanceOf(feign.httpclient.ApacheHttpClient.class)));
+ }
+
+ @Data
+ @AllArgsConstructor
+ @NoArgsConstructor
+ public static class Hello {
+ private String message;
+ }
+
+ // Load balancer with fixed server list for "local" pointing to localhost
+ @Configuration
+ static class LocalRibbonClientConfiguration {
+
+ @Value("${local.server.port}")
+ private int port = 0;
+
+ @Bean
+ public ILoadBalancer ribbonLoadBalancer() {
+ BaseLoadBalancer balancer = new BaseLoadBalancer();
+ balancer.setServersList(Arrays.asList(new Server("localhost", this.port)));
+ return balancer;
+ }
+
+ }
+}