From 6dc0bd5f5e121d89b4fa0926517ec42744e8388e Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 29 Jan 2015 17:38:38 +0000 Subject: [PATCH] Refactor @FeignClient to use name= or url= but not both Replaces the boolean flag (loadbalance) for switching between service resolution (by name) or straight URL bashing. --- .../main/asciidoc/spring-cloud-netflix.adoc | 129 +++++++++--------- .../netflix/feign/FeignAutoConfiguration.java | 15 -- .../cloud/netflix/feign/FeignClient.java | 18 ++- .../netflix/feign/FeignClientFactoryBean.java | 29 ++-- .../feign/FeignClientsConfiguration.java | 3 + .../netflix/feign/FeignClientsRegistrar.java | 23 +++- .../FeignRibbonClientAutoConfiguration.java | 45 ++++++ .../feign/{ => support}/FeignUtils.java | 2 +- .../feign/{ => support}/SpringDecoder.java | 6 +- .../feign/{ => support}/SpringEncoder.java | 7 +- .../{ => support}/SpringMvcContract.java | 2 +- .../main/resources/META-INF/spring.factories | 1 + 12 files changed, 178 insertions(+), 102 deletions(-) create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientAutoConfiguration.java rename spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/{ => support}/FeignUtils.java (95%) rename spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/{ => support}/SpringDecoder.java (95%) rename spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/{ => support}/SpringEncoder.java (96%) rename spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/{ => support}/SpringMvcContract.java (99%) diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index 9058ceb2..9598b898 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -425,65 +425,6 @@ On the server side Just create a Spring Boot application and annotate it with `@ Spring Cloud provides a `spring-cloud-starter-turbine-amqp` that has all the dependencies you need to get a Turbine AMQP server running. You need Java 8 to run the app because it is Netty-based. -[[spring-cloud-feign]] -== Declarative REST Client: Feign - -https://github.com/Netflix/feign[Feign] is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same `HttpMessageConverters` used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client when using Feign. - -Example spring boot app - -[source,java,indent=0] ----- -@Configuration -@ComponentScan -@EnableAutoConfiguration -@EnableEurekaClient -@FeignClientScan -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - -} ----- - -.StoreClient.java -[source,java,indent=0] ----- -@FeignClient("stores") -public interface StoreClient { - @RequestMapping(method = RequestMethod.GET, value = "/stores") - List getStores(); - - @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json") - Store update(@PathParameter("storeId") Long storeId, Store store); -} ----- - -In the `@FeignClient` annotation the String value ("stores" above) is -the arbitrary name of the client, used to create a configuration -prefix (see <>). - -[[spring-cloud-feign-without-eureka]] -=== Example: How to Use Feign Without Eureka - -Eureka is a convenient way to abstract the discovery of remote servers -so you don't have to hard code their URLs in clients, but if you -prefer not to use it, Ribbon and Feign are still quite -amenable. Suppose you have declared a Feign client as above for -"stores", and Eureka is not in use (and not even on the -classpath). You should find that the Ribbon client defaults to a -configured server list, and you can supply the configuration like this - -.application.yml ----- -stores: - ribbon: - listOfServers: example.com,google.com ----- - [[spring-cloud-ribbon]] == Client Side Load Balancer: Ribbon @@ -505,8 +446,7 @@ annotation). Spring Cloud creates a new ensemble as an You can configure some bits of a Ribbon client using external properties in `.ribbon.*`, which is no different than using the Netflix APIs natively, except that you can use Spring Boot -configuration files (example -<>). The native options can +configuration files. The native options can be inspected as static fields in `CommonClientConfigKey` (part of ribbon-core). @@ -561,6 +501,24 @@ populates the list of servers from Eureka. It also replaces the `IPing` interface with `NIWSDiscoveryPing` which delegates to Eureka to determine if a server is up. +[[spring-cloud-ribbon-without-eureka]] +=== Example: How to Use Ribbon Without Eureka + +Eureka is a convenient way to abstract the discovery of remote servers +so you don't have to hard code their URLs in clients, but if you +prefer not to use it, Ribbon and Feign are still quite +amenable. Suppose you have declared a `@RibbonClient` for "stores", +and Eureka is not in use (and not even on the classpath). The Ribbon +client defaults to a configured server list, and you can supply the +configuration like this + +.application.yml +---- +stores: + ribbon: + listOfServers: example.com,google.com +---- + === Using the Ribbon API Directly You can also use the `LoadBalancerClient` directly. Example: @@ -603,6 +561,55 @@ physical address. See {github-code}/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfiguration.java[RibbonAutoConfiguration] for details of how the `RestTemplate` is set up. +[[spring-cloud-feign]] +== Declarative REST Client: Feign + +https://github.com/Netflix/feign[Feign] is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same `HttpMessageConverters` used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client when using Feign. + +Example spring boot app + +[source,java,indent=0] +---- +@Configuration +@ComponentScan +@EnableAutoConfiguration +@EnableEurekaClient +@EnableFeignClients +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} +---- + +.StoreClient.java +[source,java,indent=0] +---- +@FeignClient("stores") +public interface StoreClient { + @RequestMapping(method = RequestMethod.GET, value = "/stores") + List getStores(); + + @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json") + Store update(@PathParameter("storeId") Long storeId, Store store); +} +---- + +In the `@FeignClient` annotation the String value ("stores" above) is +an arbitrary client name, which is used to create a Ribbon load +balancer (see <>). You can also specify a URL using the `url` attribute +(absolute value or just a hostname). + +The Ribbon client above will want to discover the physical addresses +for the "stores" service. If your application is a Eureka client then +it will resolve the service in the Eureka service registry. If you +don't want to use Eureka, you can simply configure a list of servers +in your external configuration (see +<>). + == External Configuration: Archaius https://github.com/Netflix/archaius[Archaius] is the Netflix client side configuration library. It is the library used by all of the Netflix OSS components for configuration. Archaius is an extension of the http://commons.apache.org/proper/commons-configuration[Apache Commons Configuration] project. It allows updates to configuration by either polling a source for changes or for a source to push changes to the client. Archaius uses DynamicProperty classes as handles to properties. diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignAutoConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignAutoConfiguration.java index 088c433e..37f99bd9 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignAutoConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignAutoConfiguration.java @@ -19,14 +19,8 @@ package org.springframework.cloud.netflix.feign; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration; -import org.springframework.cloud.netflix.feign.ribbon.FeignRibbonClient; -import org.springframework.cloud.netflix.ribbon.SpringClientFactory; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.netflix.loadbalancer.ILoadBalancer; - -import feign.Client; import feign.Feign; /** @@ -39,13 +33,4 @@ import feign.Feign; @EnableFeignClients public class FeignAutoConfiguration { - @ConditionalOnClass(ILoadBalancer.class) - @Configuration - protected static class RibbonClientConfiguration { - @Bean - public Client feignRibbonClient(SpringClientFactory factory) { - return new FeignRibbonClient(factory); - } - } - } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java index e8d02c26..46b28fa0 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java @@ -36,16 +36,20 @@ import java.lang.annotation.Target; public @interface FeignClient { /** - * The serviceId if loadbalance is true, or an absolute URL otherwise There is no need - * to prefix serviceId with http://. + * The serviceId with optional protocol prefix. Synonym for {@link #serviceId() + * serviceId}. Either serviceId or url must be specified but not both. */ - String value(); + String value() default ""; /** - * Set to true if calls should be load balanced (assuming a load balancer is - * available). If no load balancer is available this flag is ignored (and hence the - * {@link #value() value} should be an absolute URL). + * The serviceId with optional protocol prefix. Synonym for {@link #value() value}. + * Either serviceId or url must be specified but not both. */ - boolean loadbalance() default true; + String serviceId() default ""; + + /** + * An absolute URL or resolvable hostname (the protocol is optional). + */ + String url() default ""; } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactoryBean.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactoryBean.java index 1731f3d2..587fa04a 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactoryBean.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactoryBean.java @@ -22,7 +22,10 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import feign.Client; import feign.Contract; @@ -42,13 +45,13 @@ import feign.slf4j.Slf4jLogger; */ @Data @EqualsAndHashCode(callSuper = false) -class FeignClientFactoryBean implements FactoryBean { - - private boolean loadbalance; +class FeignClientFactoryBean implements FactoryBean, InitializingBean { private Class type; - private String schemeName; + private String name; + + private String url; @Autowired private Decoder decoder; @@ -80,6 +83,14 @@ class FeignClientFactoryBean implements FactoryBean { @Autowired(required = false) private List requestInterceptors; + @Override + public void afterPropertiesSet() throws Exception { + if (StringUtils.hasText(this.name)) { + Assert.state(!StringUtils.hasText(this.url), + "Either value or url can be specified, but not both"); + } + } + protected Feign.Builder feign() { Feign.Builder builder = Feign.builder() // required values @@ -118,13 +129,13 @@ class FeignClientFactoryBean implements FactoryBean { @Override public Object getObject() throws Exception { - if (!this.schemeName.startsWith("http")) { - this.schemeName = "http://" + this.schemeName; + if (StringUtils.hasText(this.name) && !this.name.startsWith("http")) { + this.name = "http://" + this.name; } - if (this.loadbalance) { - return loadBalance(feign(), this.type, this.schemeName); + if (StringUtils.hasText(this.name)) { + return loadBalance(feign(), this.type, this.name); } - return feign().target(this.type, this.schemeName); + return feign().target(this.type, this.url); } @Override 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 2c317f8e..2be24148 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,6 +16,9 @@ package org.springframework.cloud.netflix.feign; +import org.springframework.cloud.netflix.feign.support.SpringDecoder; +import org.springframework.cloud.netflix.feign.support.SpringEncoder; +import org.springframework.cloud.netflix.feign.support.SpringMvcContract; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java index 6fda8be8..4d7ac511 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java @@ -99,15 +99,34 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, String className = annotationMetadata.getClassName(); BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); - definition.addPropertyValue("loadbalance", attributes.get("loadbalance")); + validate(attributes); + definition.addPropertyValue("url", getUrl(attributes)); + definition.addPropertyValue("name", getServiceId(attributes)); definition.addPropertyValue("type", className); - definition.addPropertyValue("schemeName", attributes.get("value")); String beanName = StringUtils.uncapitalize(className.substring(className .lastIndexOf(".") + 1)); return new BeanDefinitionHolder(definition.getBeanDefinition(), beanName); } + private void validate(Map attributes) { + if (StringUtils.hasText((String) attributes.get("value"))) { + Assert.isTrue(!StringUtils.hasText((String) attributes.get("name")), + "Either name or value can be specified, but not both"); + } + } + + private String getServiceId(Map attributes) { + if (StringUtils.hasText((String) attributes.get("name"))) { + return (String) attributes.get("name"); + } + return (String) attributes.get("value"); + } + + private String getUrl(Map attributes) { + return (String) attributes.get("url"); + } + protected ClassPathScanningCandidateComponentProvider getScanner() { return new ClassPathScanningCandidateComponentProvider(false) { 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 new file mode 100644 index 00000000..455d43a4 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientAutoConfiguration.java @@ -0,0 +1,45 @@ +/* + * 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.ribbon; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.netflix.feign.FeignAutoConfiguration; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.netflix.loadbalancer.ILoadBalancer; + +import feign.Client; +import feign.Feign; + +/** + * Autoconfiguration to be activated if Feign is in use and needs to be use Ribbon as a + * load balancer. + * + * @author Dave Syer + */ +@ConditionalOnClass({ ILoadBalancer.class, Feign.class }) +@Configuration +@AutoConfigureBefore(FeignAutoConfiguration.class) +public class FeignRibbonClientAutoConfiguration { + @Bean + public Client feignRibbonClient(SpringClientFactory factory) { + return new FeignRibbonClient(factory); + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignUtils.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/FeignUtils.java similarity index 95% rename from spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignUtils.java rename to spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/FeignUtils.java index 2c7dadb9..c7b6526a 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignUtils.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/FeignUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.netflix.feign; +package org.springframework.cloud.netflix.feign.support; import java.util.ArrayList; import java.util.Collection; diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/SpringDecoder.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringDecoder.java similarity index 95% rename from spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/SpringDecoder.java rename to spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringDecoder.java index b4723ec5..3b5813d3 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/SpringDecoder.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringDecoder.java @@ -14,9 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.netflix.feign; - -import static org.springframework.cloud.netflix.feign.FeignUtils.getHttpHeaders; +package org.springframework.cloud.netflix.feign.support; import java.io.IOException; import java.io.InputStream; @@ -32,6 +30,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpResponse; import org.springframework.web.client.HttpMessageConverterExtractor; +import static org.springframework.cloud.netflix.feign.support.FeignUtils.getHttpHeaders; + import feign.FeignException; import feign.Response; import feign.codec.DecodeException; diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/SpringEncoder.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringEncoder.java similarity index 96% rename from spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/SpringEncoder.java rename to spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringEncoder.java index be8646a4..68d8e705 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/SpringEncoder.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.netflix.feign; +package org.springframework.cloud.netflix.feign.support; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -24,6 +24,7 @@ import java.util.Collection; import javax.inject.Provider; import lombok.extern.apachecommons.CommonsLog; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.web.HttpMessageConverters; import org.springframework.http.HttpHeaders; @@ -33,12 +34,12 @@ import org.springframework.http.converter.HttpMessageConverter; import com.google.common.base.Charsets; +import static org.springframework.cloud.netflix.feign.support.FeignUtils.getHttpHeaders; + import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; -import static org.springframework.cloud.netflix.feign.FeignUtils.getHttpHeaders; - /** * @author Spencer Gibb */ diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/SpringMvcContract.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java similarity index 99% rename from spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/SpringMvcContract.java rename to spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java index c2695176..f678bf87 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/SpringMvcContract.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.netflix.feign; +package org.springframework.cloud.netflix.feign.support; import java.lang.annotation.Annotation; import java.lang.reflect.Method; diff --git a/spring-cloud-netflix-core/src/main/resources/META-INF/spring.factories b/spring-cloud-netflix-core/src/main/resources/META-INF/spring.factories index 87bc62f8..efee3500 100644 --- a/spring-cloud-netflix-core/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-netflix-core/src/main/resources/META-INF/spring.factories @@ -1,6 +1,7 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration,\ org.springframework.cloud.netflix.config.EurekaClientConfigServerAutoConfiguration,\ +org.springframework.cloud.netflix.feign.ribbon.FeignRibbonClientAutoConfiguration,\ org.springframework.cloud.netflix.feign.FeignAutoConfiguration,\ org.springframework.cloud.netflix.hystrix.HystrixAutoConfiguration,\ org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\