Browse Source

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

Adds `@FeignClient.fallbackFactory` to define a `feign.hystrix.FallbackFactory`.

fixes gh-1117
pull/6/head
bpicode 9 years ago committed by Spencer Gibb
parent
commit
8e18c3d3b6
  1. 24
      docs/src/main/asciidoc/spring-cloud-netflix.adoc
  2. 9
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java
  3. 2
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactoryBean.java
  4. 1
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java
  5. 66
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/HystrixTargeter.java
  6. 124
      spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/invalid/FeignClientValidationTests.java
  7. 47
      spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java

24
docs/src/main/asciidoc/spring-cloud-netflix.adoc

@ -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`.
[[spring-cloud-feign-inheritance]]

9
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java

@ -90,6 +90,15 @@ public @interface FeignClient { @@ -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
* <code>@RibbonClient</code>.

2
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactoryBean.java

@ -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<?> fallbackFactory = void.class;
@Override
public void afterPropertiesSet() throws Exception {
Assert.hasText(this.name, "Name must be set");

1
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java

@ -179,6 +179,7 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, @@ -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";

66
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/HystrixTargeter.java

@ -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.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 { @@ -29,26 +32,67 @@ class HystrixTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> 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> 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) {
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;
}
}

124
spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/invalid/FeignClientValidationTests.java

@ -16,6 +16,7 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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";
}
}
}
}

47
spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java

@ -74,6 +74,7 @@ import feign.Client; @@ -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 { @@ -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 { @@ -237,6 +241,27 @@ public class FeignClientTests {
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 {
@Override
public Hello fail() {
@ -268,13 +293,15 @@ public class FeignClientTests { @@ -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 { @@ -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 { @@ -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);

Loading…
Cancel
Save