Add support for fallback factories in feign client annotation. (#1373)
Adds `@FeignClient.fallbackFactory` to define a `feign.hystrix.FallbackFactory`. fixes gh-1117
This commit is contained in:
@@ -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]]
|
||||
|
||||
@@ -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>.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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> 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);
|
||||
}
|
||||
|
||||
Object fallbackInstance = context.getInstance(factory.getName(), factory.getFallback());
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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;
|
||||
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<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 {
|
||||
@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);
|
||||
|
||||
Reference in New Issue
Block a user