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.
This commit is contained in:
Dave Syer
2015-01-29 17:38:38 +00:00
parent 9aa979cd31
commit 6dc0bd5f5e
12 changed files with 178 additions and 102 deletions

View File

@@ -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<Store> 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-ribbon,below for details of Ribbon
support>>).
[[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 `<client>.ribbon.*`, which is no different than using
the Netflix APIs natively, except that you can use Spring Boot
configuration files (example
<<spring-cloud-feign-without-eureka,above>>). 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<Store> 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 <<spring-cloud-ribbon,below for details of Ribbon
support>>). 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
<<spring-cloud-ribbon-without-eureka,above for example>>).
== 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 Dynamic<Type>Property classes as handles to properties.

View File

@@ -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);
}
}
}

View File

@@ -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 "";
}

View File

@@ -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<Object> {
private boolean loadbalance;
class FeignClientFactoryBean implements FactoryBean<Object>, 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<Object> {
@Autowired(required = false)
private List<RequestInterceptor> 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<Object> {
@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

View File

@@ -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;

View File

@@ -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<String, Object> 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<String, Object> attributes) {
if (StringUtils.hasText((String) attributes.get("name"))) {
return (String) attributes.get("name");
}
return (String) attributes.get("value");
}
private String getUrl(Map<String, Object> attributes) {
return (String) attributes.get("url");
}
protected ClassPathScanningCandidateComponentProvider getScanner() {
return new ClassPathScanningCandidateComponentProvider(false) {

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -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,\