Skip to content

Commit

Permalink
Support feign hystrix fallbacks.
Browse files Browse the repository at this point in the history
Added @FeignClient(fallback).

Renamed FeignClientFactory to FeignContext.

fixes gh-762
  • Loading branch information
spencergibb committed Jan 14, 2016
1 parent 390f001 commit f00f761
Show file tree
Hide file tree
Showing 14 changed files with 392 additions and 84 deletions.
23 changes: 23 additions & 0 deletions docs/src/main/asciidoc/spring-cloud-netflix.adoc
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -860,6 +860,29 @@ public class FooConfiguration {
} }
---- ----


[[spring-cloud-feign-hystrix-fallback]]
=== Feign Hystrix Fallbacks

Hystrix supports the notion of a fallback: a default code path that is executed when they circuit is open or there is an error. To enable fallbacks for a given `@FeignClient` set the `fallback` attribute to the class name that implements the fallback.

[source,java,indent=0]
----
@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}
static class HystrixClientFallback implements HystrixClient {
@Override
public Hello iFailSometimes() {
return new Hello("fallback");
}
}
----

WARNING: There is a limitation with the implementation of fallbacks in Feign and how Hystrix fallbacks work. Fallbacks are currently not supported for methods that return `com.netflix.hystrix.HystrixCommand` and `rx.Observable`.

This comment has been minimized.

Copy link
@codefromthecrypt

codefromthecrypt Jan 15, 2016

raised this to fix it upstream. missing tests (are my fault)! OpenFeign/feign#317


[[spring-cloud-feign-inheritance]] [[spring-cloud-feign-inheritance]]
=== Feign Inheritance Support === Feign Inheritance Support


Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ public HasFeatures feignFeature() {
} }


@Bean @Bean
public FeignClientFactory feignClientFactory() { public FeignContext feignContext() {
FeignClientFactory factory = new FeignClientFactory(); FeignContext context = new FeignContext();
factory.setConfigurations(this.configurations); context.setConfigurations(this.configurations);
return factory; return context;
} }
} }
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -76,4 +76,10 @@
* @see FeignClientsConfiguration for the defaults * @see FeignClientsConfiguration for the defaults
*/ */
Class<?>[] configuration() default {}; Class<?>[] configuration() default {};

/**
* Fallback class for the specified Feign client interface. The fallback class must
* implement the interface annotated by this annotation and be a valid spring bean.
*/
Class<?> fallback() default void.class;
} }
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;


import feign.Client; import feign.Client;
Expand All @@ -33,7 +34,6 @@
import feign.Request; import feign.Request;
import feign.RequestInterceptor; import feign.RequestInterceptor;
import feign.Retryer; import feign.Retryer;
import feign.Target;
import feign.Target.HardCodedTarget; import feign.Target.HardCodedTarget;
import feign.codec.Decoder; import feign.codec.Decoder;
import feign.codec.Encoder; import feign.codec.Encoder;
Expand All @@ -47,7 +47,22 @@
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware { class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
ApplicationContextAware {

private static final Targeter targeter;

static {
Targeter targeterToUse;
if (ClassUtils.isPresent("feign.hystrix.HystrixFeign",
FeignClientFactoryBean.class.getClassLoader())) {
targeterToUse = new HystrixTargeter();
}
else {
targeterToUse = new DefaultTargeter();
}
targeter = targeterToUse;
}


private Class<?> type; private Class<?> type;


Expand All @@ -57,7 +72,9 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, A


private boolean decode404; private boolean decode404;


private ApplicationContext context; private ApplicationContext applicationContext;

private Class<?> fallback = void.class;


@Override @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
Expand All @@ -66,43 +83,44 @@ public void afterPropertiesSet() throws Exception {


@Override @Override
public void setApplicationContext(ApplicationContext context) throws BeansException { public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context; this.applicationContext = context;
} }


protected Feign.Builder feign(FeignClientFactory factory) { protected Feign.Builder feign(FeignContext context) {
Logger logger = getOptional(factory, Logger.class); Logger logger = getOptional(context, Logger.class);


if (logger == null) { if (logger == null) {
logger = new Slf4jLogger(this.type); logger = new Slf4jLogger(this.type);
} }


// @formatter:off // @formatter:off
Feign.Builder builder = get(factory, Feign.Builder.class) Feign.Builder builder = get(context, Feign.Builder.class)
// required values // required values
.logger(logger) .logger(logger)
.encoder(get(factory, Encoder.class)) .encoder(get(context, Encoder.class))
.decoder(get(factory, Decoder.class)) .decoder(get(context, Decoder.class))
.contract(get(factory, Contract.class)); .contract(get(context, Contract.class));
// @formatter:on // @formatter:on


// optional values // optional values
Logger.Level level = getOptional(factory, Logger.Level.class); Logger.Level level = getOptional(context, Logger.Level.class);
if (level != null) { if (level != null) {
builder.logLevel(level); builder.logLevel(level);
} }
Retryer retryer = getOptional(factory, Retryer.class); Retryer retryer = getOptional(context, Retryer.class);
if (retryer != null) { if (retryer != null) {
builder.retryer(retryer); builder.retryer(retryer);
} }
ErrorDecoder errorDecoder = getOptional(factory, ErrorDecoder.class); ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
if (errorDecoder != null) { if (errorDecoder != null) {
builder.errorDecoder(errorDecoder); builder.errorDecoder(errorDecoder);
} }
Request.Options options = getOptional(factory, Request.Options.class); Request.Options options = getOptional(context, Request.Options.class);
if (options != null) { if (options != null) {
builder.options(options); builder.options(options);
} }
Map<String, RequestInterceptor> requestInterceptors = factory.getInstances(this.name, RequestInterceptor.class); Map<String, RequestInterceptor> requestInterceptors = context.getInstances(
this.name, RequestInterceptor.class);
if (requestInterceptors != null) { if (requestInterceptors != null) {
builder.requestInterceptors(requestInterceptors.values()); builder.requestInterceptors(requestInterceptors.values());
} }
Expand All @@ -114,44 +132,52 @@ protected Feign.Builder feign(FeignClientFactory factory) {
return builder; return builder;
} }


protected <T> T get(FeignClientFactory factory, Class<T> type) { protected <T> T get(FeignContext context, Class<T> type) {
T instance = factory.getInstance(this.name, type); T instance = context.getInstance(this.name, type);
if (instance == null) { if (instance == null) {
throw new IllegalStateException("No bean found of type " + type + " for " + this.name); throw new IllegalStateException("No bean found of type " + type + " for "
+ this.name);
} }
return instance; return instance;
} }


protected <T> T getOptional(FeignClientFactory factory, Class<T> type) { protected <T> T getOptional(FeignContext context, Class<T> type) {
return factory.getInstance(this.name, type); return context.getInstance(this.name, type);
} }


protected <T> T loadBalance(Feign.Builder builder, FeignClientFactory factory, Target<T> target) { protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
Client client = getOptional(factory, Client.class); HardCodedTarget<T> target) {
Client client = getOptional(context, Client.class);
if (client != null) { if (client != null) {
return builder.client(client).target(target); builder.client(client);
return targeter.target(this, builder, context, target);
} }


throw new IllegalStateException("No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?"); throw new IllegalStateException(
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?");
} }


@Override @Override
public Object getObject() throws Exception { public Object getObject() throws Exception {
FeignClientFactory factory = context.getBean(FeignClientFactory.class); FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);


if (!StringUtils.hasText(this.url)) { if (!StringUtils.hasText(this.url)) {
String url; String url;
if (!this.name.startsWith("http")) { if (!this.name.startsWith("http")) {
url = "http://" + this.name; url = "http://" + this.name;
} else { }
else {
url = this.name; url = this.name;
} }
return loadBalance(feign(factory), factory, new HardCodedTarget<>(this.type, this.name, url)); return loadBalance(builder, context, new HardCodedTarget<>(this.type,
this.name, url));
} }
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url; this.url = "http://" + this.url;
} }
return feign(factory).target(new HardCodedTarget<>(this.type, this.name, this.url)); return targeter.target(this, builder, context, new HardCodedTarget<>(
this.type, this.name, this.url));
} }


@Override @Override
Expand All @@ -164,4 +190,48 @@ public boolean isSingleton() {
return true; return true;
} }


interface Targeter {
<T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
HardCodedTarget<T> target);
}

static class DefaultTargeter implements Targeter {

@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
HardCodedTarget<T> target) {
return feign.target(target);
}
}

@SuppressWarnings("unchecked")
static class HystrixTargeter implements Targeter {

@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
HardCodedTarget<T> target) {
if (factory.fallback == void.class
|| !(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
return feign.target(target);
}

Object fallbackInstance = context.getInstance(factory.name, factory.fallback);
if (fallbackInstance == null) {
throw new IllegalStateException(String.format(
"No fallback instance of type %s found for feign client %s",
factory.fallback, factory.name));
}

if (!target.type().isAssignableFrom(factory.fallback)) {
throw new IllegalStateException(
String.format(
"Incompatible fallback instance. Fallback of type %s is not assignable to %s for feign client %s",
factory.fallback, target.type(), factory.name));
}

feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
return builder.target(target, (T) fallbackInstance);
}
}

} }
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -74,19 +74,21 @@ public Contract feignContract() {
return new SpringMvcContract(parameterProcessors); return new SpringMvcContract(parameterProcessors);
} }


@Bean @Configuration
@Scope("prototype") @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
@ConditionalOnMissingBean protected static class HystrixFeignConfiguration {
@ConditionalOnClass(HystrixCommand.class) @Bean
@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true) @Scope("prototype")
public Feign.Builder feignHystrixBuilder() { @ConditionalOnMissingBean
return HystrixFeign.builder(); @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true)
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
} }


@Bean @Bean
@Scope("prototype") @Scope("prototype")
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false, havingValue = "false")
public Feign.Builder feignBuilder() { public Feign.Builder feignBuilder() {
return Feign.builder(); return Feign.builder();
} }
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ private void registerFeignClient(BeanDefinitionRegistry registry,
definition.addPropertyValue("name", getServiceId(attributes)); definition.addPropertyValue("name", getServiceId(attributes));
definition.addPropertyValue("type", className); definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404")); definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);


String beanName = StringUtils String beanName = StringUtils
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
* @author Spencer Gibb * @author Spencer Gibb
* @author Dave Syer * @author Dave Syer
*/ */
public class FeignClientFactory extends NamedContextFactory<FeignClientSpecification> { public class FeignContext extends NamedContextFactory<FeignClientSpecification> {


public FeignClientFactory() { public FeignContext() {
super(FeignClientsConfiguration.class, "feign", "feign.client.name"); super(FeignClientsConfiguration.class, "feign", "feign.client.name");
} }


Expand Down
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.springframework.cloud.netflix.feign.support;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixThreadPoolKey;

/**
* Convenience class for implementing feign fallbacks that return {@link HystrixCommand}.
* Also useful for return types of {@link rx.Observable} and {@link java.util.concurrent.Future}.
* For those return types, just call {@link FallbackCommand#observe()} or {@link FallbackCommand#queue()} respectively.
* @author Spencer Gibb
*/
public class FallbackCommand<T> extends HystrixCommand<T> {

private T result;

public FallbackCommand(T result) {
this(result, "fallback");
}

protected FallbackCommand(T result, String groupname) {
super(HystrixCommandGroupKey.Factory.asKey(groupname));
this.result = result;
}

public FallbackCommand(T result, HystrixCommandGroupKey group) {
super(group);
this.result = result;
}

public FallbackCommand(T result, HystrixCommandGroupKey group, int executionIsolationThreadTimeoutInMilliseconds) {
super(group, executionIsolationThreadTimeoutInMilliseconds);
this.result = result;
}

public FallbackCommand(T result, HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool) {
super(group, threadPool);
this.result = result;
}

public FallbackCommand(T result, HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool, int executionIsolationThreadTimeoutInMilliseconds) {
super(group, threadPool, executionIsolationThreadTimeoutInMilliseconds);
this.result = result;
}

public FallbackCommand(T result, Setter setter) {
super(setter);
this.result = result;
}

@Override
protected T run() throws Exception {
return this.result;
}
}
Loading

0 comments on commit f00f761

Please sign in to comment.