diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index 53961116..1194d049 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -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 { + @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`. [[spring-cloud-feign-inheritance]] 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 8c2e361f..86620a92 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 @@ -90,6 +90,15 @@ public @interface FeignClient { */ 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 * @RibbonClient. 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 fa345d88..18bd5b70 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 @@ -69,6 +69,8 @@ class FeignClientFactoryBean implements FactoryBean, InitializingBean, private Class fallback = void.class; + private Class fallbackFactory = void.class; + @Override public void afterPropertiesSet() throws Exception { Assert.hasText(this.name, "Name must be set"); 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 9020c6e7..53c34ef5 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 @@ -179,6 +179,7 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, definition.addPropertyValue("type", className); definition.addPropertyValue("decode404", attributes.get("decode404")); definition.addPropertyValue("fallback", attributes.get("fallback")); + definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); String alias = name + "FeignClient"; diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/HystrixTargeter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/HystrixTargeter.java index 5b9ed8d8..66106c70 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/HystrixTargeter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/HystrixTargeter.java @@ -19,6 +19,9 @@ package org.springframework.cloud.netflix.feign; import feign.Feign; import feign.Target; +import feign.hystrix.FallbackFactory; +import feign.hystrix.HystrixFeign; +import org.springframework.util.Assert; /** * @author Spencer Gibb @@ -29,26 +32,67 @@ class HystrixTargeter implements Targeter { @Override public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget target) { - if (factory.getFallback() == void.class - || !(feign instanceof feign.hystrix.HystrixFeign.Builder)) { + if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) { 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 targetWithFallbackFactory(String feignClientName, FeignContext context, + Target.HardCodedTarget target, + HystrixFeign.Builder builder, + Class fallbackFactoryClass) { + FallbackFactory fallbackFactory = (FallbackFactory) + 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 targetWithFallback(String feignClientName, FeignContext context, + Target.HardCodedTarget target, + HystrixFeign.Builder builder, Class fallback) { + T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type()); + return builder.target(target, fallbackInstance); + } + + private T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context, + Class beanType, Class targetType) { + Object fallbackInstance = context.getInstance(feignClientName, beanType); if (fallbackInstance == null) { throw new IllegalStateException(String.format( - "No fallback instance of type %s found for feign client %s", - factory.getFallback(), factory.getName())); + "No " + fallbackMechanism + " instance of type %s found for feign client %s", + beanType, feignClientName)); } - if (!target.type().isAssignableFrom(factory.getFallback())) { + if (!targetType.isAssignableFrom(beanType)) { throw new IllegalStateException( String.format( - "Incompatible fallback instance. Fallback of type %s is not assignable to %s for feign client %s", - factory.getFallback(), target.type(), factory.getName())); + "Incompatible " + fallbackMechanism + " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", + beanType, targetType, feignClientName)); } - - feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign; - return builder.target(target, (T) fallbackInstance); + return (T) fallbackInstance; } } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/invalid/FeignClientValidationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/invalid/FeignClientValidationTests.java index 178fc643..bd7d235f 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/invalid/FeignClientValidationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/invalid/FeignClientValidationTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.netflix.feign.invalid; +import feign.hystrix.FallbackFactory; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -42,10 +43,7 @@ public class FeignClientValidationTests { @Test public void testNameAndValue() { this.expected.expectMessage("only one is permitted"); - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - NameAndValueConfiguration.class); - assertNotNull(context.getBean(NameAndValueConfiguration.Client.class)); - context.close(); + new AnnotationConfigApplicationContext(NameAndValueConfiguration.class); } @Configuration @@ -86,10 +84,7 @@ public class FeignClientValidationTests { @Test public void testNotLegalHostname() { this.expected.expectMessage("not legal hostname (foo_bar)"); - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - BadHostnameConfiguration.class); - assertNotNull(context.getBean(BadHostnameConfiguration.Client.class)); - context.close(); + new AnnotationConfigApplicationContext(BadHostnameConfiguration.class); } @Configuration @@ -107,11 +102,12 @@ public class FeignClientValidationTests { @Test public void testMissingFallback() { - this.expected.expectMessage("No fallback instance of type"); + try ( AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - MissingFallbackConfiguration.class); - assertNotNull(context.getBean(MissingFallbackConfiguration.Client.class)); - context.close(); + MissingFallbackConfiguration.class)) { + this.expected.expectMessage("No fallback instance of type"); + assertNotNull(context.getBean(MissingFallbackConfiguration.Client.class)); + } } @Configuration @@ -136,11 +132,11 @@ public class FeignClientValidationTests { @Test public void testWrongFallbackType() { - this.expected.expectMessage("Incompatible fallback instance"); - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - WrongFallbackTypeConfiguration.class); - assertNotNull(context.getBean(WrongFallbackTypeConfiguration.Client.class)); - context.close(); + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + WrongFallbackTypeConfiguration.class)) { + this.expected.expectMessage("Incompatible fallback instance"); + assertNotNull(context.getBean(WrongFallbackTypeConfiguration.Client.class)); + } } @Configuration @@ -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 { + + @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 { + + @Override + public String create(Throwable cause) { + return "tryinToTrickYa"; + } + } + + } } 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 49f3b831..366e6454 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 @@ -74,6 +74,7 @@ import feign.Client; import feign.Logger; import feign.RequestInterceptor; import feign.RequestTemplate; +import feign.hystrix.FallbackFactory; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -115,6 +116,9 @@ public class FeignClientTests { @Autowired HystrixClient hystrixClient; + @Autowired + private HystrixClientWithFallBackFactory hystrixClientWithFallBackFactory; + @Autowired @Qualifier("localapp3FeignClient") HystrixClient namedHystrixClient; @@ -237,6 +241,27 @@ public class FeignClientTests { Future failFuture(); } + @FeignClient(name = "localapp4", fallbackFactory = HystrixClientFallbackFactory.class) + protected interface HystrixClientWithFallBackFactory { + + @RequestMapping(method = RequestMethod.GET, path = "/fail") + Hello fail(); + } + + static class HystrixClientFallbackFactory implements FallbackFactory { + + @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 { @Override public Hello fail() { @@ -268,13 +293,15 @@ public class FeignClientTests { @EnableAutoConfiguration @RestController @EnableFeignClients(clients = { TestClientServiceId.class, TestClient.class, - DecodingTestClient.class, - HystrixClient.class }, defaultConfiguration = TestDefaultFeignConfig.class) + DecodingTestClient.class, HystrixClient.class, HystrixClientWithFallBackFactory.class }, + defaultConfiguration = TestDefaultFeignConfig.class) @RibbonClients({ @RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class), @RibbonClient(name = "localapp1", 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 { // needs to be in parent context to test multiple HystrixClient beans @@ -283,6 +310,11 @@ public class FeignClientTests { return new HystrixClientFallback(); } + @Bean + public HystrixClientFallbackFactory hystrixClientFallbackFactory() { + return new HystrixClientFallbackFactory(); + } + @Bean FeignFormatterRegistrar feignFormatterRegistrar() { return new FeignFormatterRegistrar() { @@ -584,6 +616,15 @@ public class FeignClientTests { assertEquals("message was wrong", "fallbackfuture", hello.getMessage()); } + @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() { assertNotNull("namedHystrixClient was null", this.namedHystrixClient);