Commit 8e18c3d3 by bpicode Committed by Spencer Gibb

Add support for fallback factories in feign client annotation. (#1373)

Adds `@FeignClient.fallbackFactory` to define a `feign.hystrix.FallbackFactory`. fixes gh-1117
parent 0adbd566
...@@ -1012,6 +1012,30 @@ static class HystrixClientFallback implements HystrixClient { ...@@ -1012,6 +1012,30 @@ static class HystrixClientFallback implements HystrixClient {
} }
---- ----
If one needs access to the cause that made the fallback trigger, one can use the `fallbackFactory` attribute inside `@FeignClient`.
[source,java,indent=0]
----
@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}
@Component
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
@Override
public HystrixClient create(Throwable cause) {
return new HystrixClientWithFallBackFactory() {
@Override
public Hello iFailSometimes() {
return new Hello("fallback; reason was: " + cause.getMessage());
}
};
}
}
----
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`. 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`.
[[spring-cloud-feign-inheritance]] [[spring-cloud-feign-inheritance]]
......
...@@ -91,6 +91,15 @@ public @interface FeignClient { ...@@ -91,6 +91,15 @@ public @interface FeignClient {
Class<?> fallback() default void.class; Class<?> fallback() default void.class;
/** /**
* Define a fallback factory for the specified Feign client interface. The fallback
* factory must produce instances of fallback classes that implement the interface
* annotated by {@link FeignClient}.
*
* @see feign.hystrix.FallbackFactory for details.
*/
Class<?> fallbackFactory() default void.class;
/**
* Path prefix to be used by all method-level mappings. Can be used with or without * Path prefix to be used by all method-level mappings. Can be used with or without
* <code>@RibbonClient</code>. * <code>@RibbonClient</code>.
*/ */
......
...@@ -69,6 +69,8 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ...@@ -69,6 +69,8 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
private Class<?> fallback = void.class; private Class<?> fallback = void.class;
private Class<?> fallbackFactory = void.class;
@Override @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
Assert.hasText(this.name, "Name must be set"); Assert.hasText(this.name, "Name must be set");
......
...@@ -179,6 +179,7 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ...@@ -179,6 +179,7 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
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.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = name + "FeignClient"; String alias = name + "FeignClient";
......
...@@ -19,6 +19,9 @@ package org.springframework.cloud.netflix.feign; ...@@ -19,6 +19,9 @@ package org.springframework.cloud.netflix.feign;
import feign.Feign; import feign.Feign;
import feign.Target; import feign.Target;
import feign.hystrix.FallbackFactory;
import feign.hystrix.HystrixFeign;
import org.springframework.util.Assert;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
...@@ -29,26 +32,67 @@ class HystrixTargeter implements Targeter { ...@@ -29,26 +32,67 @@ class HystrixTargeter implements Targeter {
@Override @Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) { Target.HardCodedTarget<T> target) {
if (factory.getFallback() == void.class if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
|| !(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
return feign.target(target); return feign.target(target);
} }
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
Class<?> fallback = factory.getFallback();
if (fallback != void.class) {
return targetWithFallback(factory.getName(), context, target, builder, fallback);
}
Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) {
return targetWithFallbackFactory(factory.getName(), context, target, builder, fallbackFactory);
}
return feign.target(target);
}
private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context,
Target.HardCodedTarget<T> target,
HystrixFeign.Builder builder,
Class<?> fallbackFactoryClass) {
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>)
getFromContext("fallbackFactory", feignClientName, context, fallbackFactoryClass, FallbackFactory.class);
/* We take a sample fallback from the fallback factory to check if it returns a fallback
that is compatible with the annotated feign interface. */
Object exampleFallback = fallbackFactory.create(new RuntimeException());
Assert.notNull(exampleFallback,
String.format(
"Incompatible fallbackFactory instance for feign client %s. Factory may not produce null!",
feignClientName));
if (!target.type().isAssignableFrom(exampleFallback.getClass())) {
throw new IllegalStateException(
String.format(
"Incompatible fallbackFactory instance for feign client %s. Factory produces instances of '%s', but should produce instances of '%s'",
feignClientName, exampleFallback.getClass(), target.type()));
}
return builder.target(target, fallbackFactory);
}
Object fallbackInstance = context.getInstance(factory.getName(), factory.getFallback());
private <T> T targetWithFallback(String feignClientName, FeignContext context,
Target.HardCodedTarget<T> target,
HystrixFeign.Builder builder, Class<?> fallback) {
T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type());
return builder.target(target, fallbackInstance);
}
private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context,
Class<?> beanType, Class<T> targetType) {
Object fallbackInstance = context.getInstance(feignClientName, beanType);
if (fallbackInstance == null) { if (fallbackInstance == null) {
throw new IllegalStateException(String.format( throw new IllegalStateException(String.format(
"No fallback instance of type %s found for feign client %s", "No " + fallbackMechanism + " instance of type %s found for feign client %s",
factory.getFallback(), factory.getName())); beanType, feignClientName));
} }
if (!target.type().isAssignableFrom(factory.getFallback())) { if (!targetType.isAssignableFrom(beanType)) {
throw new IllegalStateException( throw new IllegalStateException(
String.format( String.format(
"Incompatible fallback instance. Fallback of type %s is not assignable to %s for feign client %s", "Incompatible " + fallbackMechanism + " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
factory.getFallback(), target.type(), factory.getName())); beanType, targetType, feignClientName));
} }
return (T) fallbackInstance;
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
return builder.target(target, (T) fallbackInstance);
} }
} }
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package org.springframework.cloud.netflix.feign.invalid; package org.springframework.cloud.netflix.feign.invalid;
import feign.hystrix.FallbackFactory;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
...@@ -42,10 +43,7 @@ public class FeignClientValidationTests { ...@@ -42,10 +43,7 @@ public class FeignClientValidationTests {
@Test @Test
public void testNameAndValue() { public void testNameAndValue() {
this.expected.expectMessage("only one is permitted"); this.expected.expectMessage("only one is permitted");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( new AnnotationConfigApplicationContext(NameAndValueConfiguration.class);
NameAndValueConfiguration.class);
assertNotNull(context.getBean(NameAndValueConfiguration.Client.class));
context.close();
} }
@Configuration @Configuration
...@@ -86,10 +84,7 @@ public class FeignClientValidationTests { ...@@ -86,10 +84,7 @@ public class FeignClientValidationTests {
@Test @Test
public void testNotLegalHostname() { public void testNotLegalHostname() {
this.expected.expectMessage("not legal hostname (foo_bar)"); this.expected.expectMessage("not legal hostname (foo_bar)");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( new AnnotationConfigApplicationContext(BadHostnameConfiguration.class);
BadHostnameConfiguration.class);
assertNotNull(context.getBean(BadHostnameConfiguration.Client.class));
context.close();
} }
@Configuration @Configuration
...@@ -107,11 +102,12 @@ public class FeignClientValidationTests { ...@@ -107,11 +102,12 @@ public class FeignClientValidationTests {
@Test @Test
public void testMissingFallback() { public void testMissingFallback() {
this.expected.expectMessage("No fallback instance of type"); try (
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
MissingFallbackConfiguration.class); MissingFallbackConfiguration.class)) {
assertNotNull(context.getBean(MissingFallbackConfiguration.Client.class)); this.expected.expectMessage("No fallback instance of type");
context.close(); assertNotNull(context.getBean(MissingFallbackConfiguration.Client.class));
}
} }
@Configuration @Configuration
...@@ -136,11 +132,11 @@ public class FeignClientValidationTests { ...@@ -136,11 +132,11 @@ public class FeignClientValidationTests {
@Test @Test
public void testWrongFallbackType() { public void testWrongFallbackType() {
this.expected.expectMessage("Incompatible fallback instance"); try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( WrongFallbackTypeConfiguration.class)) {
WrongFallbackTypeConfiguration.class); this.expected.expectMessage("Incompatible fallback instance");
assertNotNull(context.getBean(WrongFallbackTypeConfiguration.Client.class)); assertNotNull(context.getBean(WrongFallbackTypeConfiguration.Client.class));
context.close(); }
} }
@Configuration @Configuration
...@@ -163,4 +159,98 @@ public class FeignClientValidationTests { ...@@ -163,4 +159,98 @@ public class FeignClientValidationTests {
} }
} }
@Test
public void testMissingFallbackFactory() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
MissingFallbackFactoryConfiguration.class)) {
this.expected.expectMessage("No fallbackFactory instance of type");
assertNotNull(context.getBean(MissingFallbackFactoryConfiguration.Client.class));
}
}
@Configuration
@Import(FeignAutoConfiguration.class)
@EnableFeignClients(clients = MissingFallbackFactoryConfiguration.Client.class)
protected static class MissingFallbackFactoryConfiguration {
@FeignClient(name = "foobar", url = "http://localhost", fallbackFactory = ClientFallback.class)
interface Client {
@RequestMapping(method = RequestMethod.GET, value = "/")
String get();
}
class ClientFallback implements FallbackFactory<Client> {
@Override
public Client create(Throwable cause) {
return null;
}
}
}
@Test
public void testWrongFallbackFactoryType() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
WrongFallbackFactoryTypeConfiguration.class)) {
this.expected.expectMessage("Incompatible fallbackFactory instance");
assertNotNull(context.getBean(WrongFallbackFactoryTypeConfiguration.Client.class));
}
}
@Configuration
@Import(FeignAutoConfiguration.class)
@EnableFeignClients(clients = WrongFallbackFactoryTypeConfiguration.Client.class)
protected static class WrongFallbackFactoryTypeConfiguration {
@FeignClient(name = "foobar", url = "http://localhost", fallbackFactory = Dummy.class)
interface Client {
@RequestMapping(method = RequestMethod.GET, value = "/")
String get();
}
@Bean
Dummy dummy() {
return new Dummy();
}
class Dummy {
}
}
@Test
public void testWrongFallbackFactoryGenericType() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
WrongFallbackFactoryGenericTypeConfiguration.class)) {
this.expected.expectMessage("Incompatible fallbackFactory instance");
assertNotNull(context.getBean(WrongFallbackFactoryGenericTypeConfiguration.Client.class));
}
}
@Configuration
@Import(FeignAutoConfiguration.class)
@EnableFeignClients(clients = WrongFallbackFactoryGenericTypeConfiguration.Client.class)
protected static class WrongFallbackFactoryGenericTypeConfiguration {
@FeignClient(name = "foobar", url = "http://localhost", fallbackFactory = ClientFallback.class)
interface Client {
@RequestMapping(method = RequestMethod.GET, value = "/")
String get();
}
@Bean
ClientFallback dummy() {
return new ClientFallback();
}
class ClientFallback implements FallbackFactory<String> {
@Override
public String create(Throwable cause) {
return "tryinToTrickYa";
}
}
}
} }
...@@ -74,6 +74,7 @@ import feign.Client; ...@@ -74,6 +74,7 @@ import feign.Client;
import feign.Logger; import feign.Logger;
import feign.RequestInterceptor; import feign.RequestInterceptor;
import feign.RequestTemplate; import feign.RequestTemplate;
import feign.hystrix.FallbackFactory;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
...@@ -116,6 +117,9 @@ public class FeignClientTests { ...@@ -116,6 +117,9 @@ public class FeignClientTests {
HystrixClient hystrixClient; HystrixClient hystrixClient;
@Autowired @Autowired
private HystrixClientWithFallBackFactory hystrixClientWithFallBackFactory;
@Autowired
@Qualifier("localapp3FeignClient") @Qualifier("localapp3FeignClient")
HystrixClient namedHystrixClient; HystrixClient namedHystrixClient;
...@@ -237,6 +241,27 @@ public class FeignClientTests { ...@@ -237,6 +241,27 @@ public class FeignClientTests {
Future<Hello> failFuture(); Future<Hello> failFuture();
} }
@FeignClient(name = "localapp4", fallbackFactory = HystrixClientFallbackFactory.class)
protected interface HystrixClientWithFallBackFactory {
@RequestMapping(method = RequestMethod.GET, path = "/fail")
Hello fail();
}
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClientWithFallBackFactory> {
@Override
public HystrixClientWithFallBackFactory create(final Throwable cause) {
return new HystrixClientWithFallBackFactory() {
@Override
public Hello fail() {
assertNotNull("Cause was null", cause);
return new Hello("Hello from the fallback side: " + cause.getMessage());
}
};
}
}
static class HystrixClientFallback implements HystrixClient { static class HystrixClientFallback implements HystrixClient {
@Override @Override
public Hello fail() { public Hello fail() {
...@@ -268,13 +293,15 @@ public class FeignClientTests { ...@@ -268,13 +293,15 @@ public class FeignClientTests {
@EnableAutoConfiguration @EnableAutoConfiguration
@RestController @RestController
@EnableFeignClients(clients = { TestClientServiceId.class, TestClient.class, @EnableFeignClients(clients = { TestClientServiceId.class, TestClient.class,
DecodingTestClient.class, DecodingTestClient.class, HystrixClient.class, HystrixClientWithFallBackFactory.class },
HystrixClient.class }, defaultConfiguration = TestDefaultFeignConfig.class) defaultConfiguration = TestDefaultFeignConfig.class)
@RibbonClients({ @RibbonClients({
@RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class), @RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class),
@RibbonClient(name = "localapp1", configuration = LocalRibbonClientConfiguration.class), @RibbonClient(name = "localapp1", configuration = LocalRibbonClientConfiguration.class),
@RibbonClient(name = "localapp2", configuration = LocalRibbonClientConfiguration.class), @RibbonClient(name = "localapp2", configuration = LocalRibbonClientConfiguration.class),
@RibbonClient(name = "localapp3", configuration = LocalRibbonClientConfiguration.class), }) @RibbonClient(name = "localapp3", configuration = LocalRibbonClientConfiguration.class),
@RibbonClient(name = "localapp4", configuration = LocalRibbonClientConfiguration.class)
})
protected static class Application { protected static class Application {
// needs to be in parent context to test multiple HystrixClient beans // needs to be in parent context to test multiple HystrixClient beans
...@@ -284,6 +311,11 @@ public class FeignClientTests { ...@@ -284,6 +311,11 @@ public class FeignClientTests {
} }
@Bean @Bean
public HystrixClientFallbackFactory hystrixClientFallbackFactory() {
return new HystrixClientFallbackFactory();
}
@Bean
FeignFormatterRegistrar feignFormatterRegistrar() { FeignFormatterRegistrar feignFormatterRegistrar() {
return new FeignFormatterRegistrar() { return new FeignFormatterRegistrar() {
...@@ -585,6 +617,15 @@ public class FeignClientTests { ...@@ -585,6 +617,15 @@ public class FeignClientTests {
} }
@Test @Test
public void testHystrixClientWithFallBackFactory() throws Exception {
Hello hello = hystrixClientWithFallBackFactory.fail();
assertNotNull("hello was null", hello);
assertNotNull("hello#message was null", hello.getMessage());
assertTrue("hello#message did not contain the cause (status code) of the fallback invocation",
hello.getMessage().contains("500"));
}
@Test
public void namedFeignClientWorks() { public void namedFeignClientWorks() {
assertNotNull("namedHystrixClient was null", this.namedHystrixClient); assertNotNull("namedHystrixClient was null", this.namedHystrixClient);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment