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