Commit f00f7619 by Spencer Gibb

Support feign hystrix fallbacks.

Added @FeignClient(fallback). Renamed FeignClientFactory to FeignContext. fixes gh-762
parent 390f0016
......@@ -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`.
[[spring-cloud-feign-inheritance]]
=== Feign Inheritance Support
......
......@@ -44,9 +44,9 @@ public class FeignAutoConfiguration {
}
@Bean
public FeignClientFactory feignClientFactory() {
FeignClientFactory factory = new FeignClientFactory();
factory.setConfigurations(this.configurations);
return factory;
public FeignContext feignContext() {
FeignContext context = new FeignContext();
context.setConfigurations(this.configurations);
return context;
}
}
......@@ -76,4 +76,10 @@ public @interface FeignClient {
* @see FeignClientsConfiguration for the defaults
*/
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;
}
......@@ -24,6 +24,7 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import feign.Client;
......@@ -33,7 +34,6 @@ import feign.Logger;
import feign.Request;
import feign.RequestInterceptor;
import feign.Retryer;
import feign.Target;
import feign.Target.HardCodedTarget;
import feign.codec.Decoder;
import feign.codec.Encoder;
......@@ -47,7 +47,22 @@ import lombok.EqualsAndHashCode;
*/
@Data
@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;
......@@ -57,7 +72,9 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, A
private boolean decode404;
private ApplicationContext context;
private ApplicationContext applicationContext;
private Class<?> fallback = void.class;
@Override
public void afterPropertiesSet() throws Exception {
......@@ -66,43 +83,44 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, A
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
this.applicationContext = context;
}
protected Feign.Builder feign(FeignClientFactory factory) {
Logger logger = getOptional(factory, Logger.class);
protected Feign.Builder feign(FeignContext context) {
Logger logger = getOptional(context, Logger.class);
if (logger == null) {
logger = new Slf4jLogger(this.type);
}
// @formatter:off
Feign.Builder builder = get(factory, Feign.Builder.class)
Feign.Builder builder = get(context, Feign.Builder.class)
// required values
.logger(logger)
.encoder(get(factory, Encoder.class))
.decoder(get(factory, Decoder.class))
.contract(get(factory, Contract.class));
.encoder(get(context, Encoder.class))
.decoder(get(context, Decoder.class))
.contract(get(context, Contract.class));
// @formatter:on
// optional values
Logger.Level level = getOptional(factory, Logger.Level.class);
Logger.Level level = getOptional(context, Logger.Level.class);
if (level != null) {
builder.logLevel(level);
}
Retryer retryer = getOptional(factory, Retryer.class);
Retryer retryer = getOptional(context, Retryer.class);
if (retryer != null) {
builder.retryer(retryer);
}
ErrorDecoder errorDecoder = getOptional(factory, ErrorDecoder.class);
ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
if (errorDecoder != null) {
builder.errorDecoder(errorDecoder);
}
Request.Options options = getOptional(factory, Request.Options.class);
Request.Options options = getOptional(context, Request.Options.class);
if (options != null) {
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) {
builder.requestInterceptors(requestInterceptors.values());
}
......@@ -114,44 +132,52 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, A
return builder;
}
protected <T> T get(FeignClientFactory factory, Class<T> type) {
T instance = factory.getInstance(this.name, type);
protected <T> T get(FeignContext context, Class<T> type) {
T instance = context.getInstance(this.name, type);
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;
}
protected <T> T getOptional(FeignClientFactory factory, Class<T> type) {
return factory.getInstance(this.name, type);
protected <T> T getOptional(FeignContext context, Class<T> type) {
return context.getInstance(this.name, type);
}
protected <T> T loadBalance(Feign.Builder builder, FeignClientFactory factory, Target<T> target) {
Client client = getOptional(factory, Client.class);
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
Client client = getOptional(context, Client.class);
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
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)) {
String url;
if (!this.name.startsWith("http")) {
url = "http://" + this.name;
} else {
}
else {
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")) {
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
......@@ -164,4 +190,48 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, A
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);
}
}
}
......@@ -74,19 +74,21 @@ public class FeignClientsConfiguration {
return new SpringMvcContract(parameterProcessors);
}
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnClass(HystrixCommand.class)
@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true)
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
@Configuration
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true)
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
}
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false, havingValue = "false")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
......
......@@ -174,6 +174,7 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
definition.addPropertyValue("name", getServiceId(attributes));
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String beanName = StringUtils
......
......@@ -26,9 +26,9 @@ import org.springframework.cloud.context.named.NamedContextFactory;
* @author Spencer Gibb
* @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");
}
......
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;
}
}
......@@ -47,34 +47,34 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
public class EnableFeignClientsTests {
@Autowired
private FeignClientFactory factory;
private FeignContext feignContext;
@Autowired
private ApplicationContext context;
@Test
public void decoderDefaultCorrect() {
ResponseEntityDecoder.class.cast(this.factory.getInstance("foo", Decoder.class));
ResponseEntityDecoder.class.cast(this.feignContext.getInstance("foo", Decoder.class));
}
@Test
public void encoderDefaultCorrect() {
SpringEncoder.class.cast(this.factory.getInstance("foo", Encoder.class));
SpringEncoder.class.cast(this.feignContext.getInstance("foo", Encoder.class));
}
@Test
public void loggerDefaultCorrect() {
Slf4jLogger.class.cast(this.factory.getInstance("foo", Logger.class));
Slf4jLogger.class.cast(this.feignContext.getInstance("foo", Logger.class));
}
@Test
public void contractDefaultCorrect() {
SpringMvcContract.class.cast(this.factory.getInstance("foo", Contract.class));
SpringMvcContract.class.cast(this.feignContext.getInstance("foo", Contract.class));
}
@Test
public void builderDefaultCorrect() {
HystrixFeign.Builder.class.cast(this.factory.getInstance("foo", Feign.Builder.class));
HystrixFeign.Builder.class.cast(this.feignContext.getInstance("foo", Feign.Builder.class));
}
@Configuration
......
......@@ -34,18 +34,18 @@ public class FeignClientFactoryTests {
public void testChildContexts() {
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
parent.refresh();
FeignClientFactory factory = new FeignClientFactory();
factory.setApplicationContext(parent);
factory.setConfigurations(Arrays.asList(getSpec("foo", FooConfig.class),
FeignContext context = new FeignContext();
context.setApplicationContext(parent);
context.setConfigurations(Arrays.asList(getSpec("foo", FooConfig.class),
getSpec("bar", BarConfig.class)));
Foo foo = factory.getInstance("foo", Foo.class);
Foo foo = context.getInstance("foo", Foo.class);
assertThat("foo was null", foo, is(notNullValue()));
Bar bar = factory.getInstance("bar", Bar.class);
Bar bar = context.getInstance("bar", Bar.class);
assertThat("bar was null", bar, is(notNullValue()));
Bar foobar = factory.getInstance("foo", Bar.class);
Bar foobar = context.getInstance("foo", Bar.class);
assertThat("bar was not null", foobar, is(nullValue()));
}
......
......@@ -58,63 +58,63 @@ import feign.slf4j.Slf4jLogger;
public class FeignClientOverrideDefaultsTests {
@Autowired
private FeignClientFactory factory;
private FeignContext context;
@Test
public void overrideDecoder() {
Decoder.Default.class.cast(this.factory.getInstance("foo", Decoder.class));
ResponseEntityDecoder.class.cast(this.factory.getInstance("bar", Decoder.class));
Decoder.Default.class.cast(this.context.getInstance("foo", Decoder.class));
ResponseEntityDecoder.class.cast(this.context.getInstance("bar", Decoder.class));
}
@Test
public void overrideEncoder() {
Encoder.Default.class.cast(this.factory.getInstance("foo", Encoder.class));
SpringEncoder.class.cast(this.factory.getInstance("bar", Encoder.class));
Encoder.Default.class.cast(this.context.getInstance("foo", Encoder.class));
SpringEncoder.class.cast(this.context.getInstance("bar", Encoder.class));
}
@Test
public void overrideLogger() {
Logger.JavaLogger.class.cast(this.factory.getInstance("foo", Logger.class));
Slf4jLogger.class.cast(this.factory.getInstance("bar", Logger.class));
Logger.JavaLogger.class.cast(this.context.getInstance("foo", Logger.class));
Slf4jLogger.class.cast(this.context.getInstance("bar", Logger.class));
}
@Test
public void overrideContract() {
Contract.Default.class.cast(this.factory.getInstance("foo", Contract.class));
SpringMvcContract.class.cast(this.factory.getInstance("bar", Contract.class));
Contract.Default.class.cast(this.context.getInstance("foo", Contract.class));
SpringMvcContract.class.cast(this.context.getInstance("bar", Contract.class));
}
@Test
public void overrideLoggerLevel() {
assertNull(this.factory.getInstance("foo", Logger.Level.class));
assertNull(this.context.getInstance("foo", Logger.Level.class));
assertEquals(Logger.Level.HEADERS,
this.factory.getInstance("bar", Logger.Level.class));
this.context.getInstance("bar", Logger.Level.class));
}
@Test
public void overrideRetryer() {
assertNull(this.factory.getInstance("foo", Retryer.class));
Retryer.Default.class.cast(this.factory.getInstance("bar", Retryer.class));
assertNull(this.context.getInstance("foo", Retryer.class));
Retryer.Default.class.cast(this.context.getInstance("bar", Retryer.class));
}
@Test
public void overrideErrorDecoder() {
assertNull(this.factory.getInstance("foo", ErrorDecoder.class));
assertNull(this.context.getInstance("foo", ErrorDecoder.class));
ErrorDecoder.Default.class
.cast(this.factory.getInstance("bar", ErrorDecoder.class));
.cast(this.context.getInstance("bar", ErrorDecoder.class));
}
@Test
public void overrideBuilder() {
Feign.Builder.class.cast(this.factory.getInstance("foo", Feign.Builder.class));
Feign.Builder.class.cast(this.context.getInstance("foo", Feign.Builder.class));
HystrixFeign.Builder.class
.cast(this.factory.getInstance("bar", Feign.Builder.class));
.cast(this.context.getInstance("bar", Feign.Builder.class));
}
@Test
public void overrideRequestOptions() {
assertNull(this.factory.getInstance("foo", Request.Options.class));
Request.Options options = this.factory.getInstance("bar", Request.Options.class);
assertNull(this.context.getInstance("foo", Request.Options.class));
Request.Options options = this.context.getInstance("bar", Request.Options.class);
assertEquals(1, options.connectTimeoutMillis());
assertEquals(1, options.readTimeoutMillis());
}
......@@ -122,9 +122,9 @@ public class FeignClientOverrideDefaultsTests {
@Test
public void addRequestInterceptor() {
assertEquals(1,
this.factory.getInstances("foo", RequestInterceptor.class).size());
this.context.getInstances("foo", RequestInterceptor.class).size());
assertEquals(2,
this.factory.getInstances("bar", RequestInterceptor.class).size());
this.context.getInstances("bar", RequestInterceptor.class).size());
}
@Configuration
......
......@@ -57,7 +57,7 @@ import static org.junit.Assert.assertNull;
public class SpringDecoderTests extends FeignClientFactoryBean {
@Autowired
FeignClientFactory factory;
FeignContext context;
@Value("${local.server.port}")
private int port = 0;
......@@ -73,7 +73,7 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
public TestClient testClient(boolean decode404) {
setType(this.getClass());
setDecode404(decode404);
return feign(factory).target(TestClient.class, "http://localhost:" + this.port);
return feign(context).target(TestClient.class, "http://localhost:" + this.port);
}
@Test
......
......@@ -23,6 +23,7 @@ import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.feign.FeignAutoConfiguration;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.bind.annotation.RequestMapping;
......@@ -39,18 +40,18 @@ public class FeignClientValidationTests {
public ExpectedException expected = ExpectedException.none();
@Test
public void invalid() {
public void testNotLegalHostname() {
this.expected.expectMessage("not legal hostname (foo_bar)");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
BadConfiguration.class);
assertNotNull(context.getBean(BadConfiguration.Client.class));
BadHostnameConfiguration.class);
assertNotNull(context.getBean(BadHostnameConfiguration.Client.class));
context.close();
}
@Configuration
@Import(FeignAutoConfiguration.class)
@EnableFeignClients(clients = BadConfiguration.Client.class)
protected static class BadConfiguration {
@EnableFeignClients(clients = BadHostnameConfiguration.Client.class)
protected static class BadHostnameConfiguration {
@FeignClient("foo_bar")
interface Client {
......@@ -60,4 +61,61 @@ public class FeignClientValidationTests {
}
@Test
public void testMissingFallback() {
this.expected.expectMessage("No fallback instance of type");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
MissingFallbackConfiguration.class);
assertNotNull(context.getBean(MissingFallbackConfiguration.Client.class));
context.close();
}
@Configuration
@Import(FeignAutoConfiguration.class)
@EnableFeignClients(clients = MissingFallbackConfiguration.Client.class)
protected static class MissingFallbackConfiguration {
@FeignClient(name = "foobar", url = "http://localhost", fallback = ClientFallback.class)
interface Client {
@RequestMapping(method = RequestMethod.GET, value = "/")
String get();
}
class ClientFallback implements Client {
@Override
public String get() {
return null;
}
}
}
@Test
public void testWrongFallbackType() {
this.expected.expectMessage("Incompatible fallback instance");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
WrongFallbackTypeConfiguration.class);
assertNotNull(context.getBean(WrongFallbackTypeConfiguration.Client.class));
context.close();
}
@Configuration
@Import(FeignAutoConfiguration.class)
@EnableFeignClients(clients = WrongFallbackTypeConfiguration.Client.class)
protected static class WrongFallbackTypeConfiguration {
@FeignClient(name = "foobar", url = "http://localhost", fallback = Dummy.class)
interface Client {
@RequestMapping(method = RequestMethod.GET, value = "/")
String get();
}
@Bean
Dummy dummy() {
return new Dummy();
}
class Dummy { }
}
}
......@@ -29,11 +29,14 @@ import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -43,6 +46,7 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.feign.support.FallbackCommand;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.cloud.netflix.feign.ribbon.LoadBalancerFeignClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
......@@ -68,6 +72,7 @@ import feign.Client;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import rx.Observable;
/**
* @author Spencer Gibb
......@@ -97,6 +102,9 @@ public class FeignClientTests {
@Autowired
private Client feignClient;
@Autowired
HystrixClient hystrixClient;
@FeignClient(value = "localapp", configuration = TestClientConfig.class)
protected interface TestClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
......@@ -159,15 +167,53 @@ public class FeignClientTests {
ResponseEntity<String> notFound();
}
@FeignClient(name = "localapp3", fallback = HystrixClientFallback.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/fail")
Hello fail();
@RequestMapping(method = RequestMethod.GET, value = "/fail")
HystrixCommand<Hello> failCommand();
@RequestMapping(method = RequestMethod.GET, value = "/fail")
Observable<Hello> failObservable();
@RequestMapping(method = RequestMethod.GET, value = "/fail")
Future<Hello> failFuture();
}
static class HystrixClientFallback implements HystrixClient {
@Override
public Hello fail() {
return new Hello("fallback");
}
@Override
public HystrixCommand<Hello> failCommand() {
return new FallbackCommand<>(new Hello("fallbackcommand"));
}
@Override
public Observable<Hello> failObservable() {
return new FallbackCommand<>(new Hello("fallbackobservable")).observe();
}
@Override
public Future<Hello> failFuture() {
return new FallbackCommand<>(new Hello("fallbackfuture")).queue();
}
}
@Configuration
@EnableAutoConfiguration
@RestController
@EnableFeignClients(clients = {TestClientServiceId.class, TestClient.class, DecodingTestClient.class},
@EnableFeignClients(clients = {TestClientServiceId.class, TestClient.class, DecodingTestClient.class, HystrixClient.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 = "localapp2", configuration = LocalRibbonClientConfiguration.class),
@RibbonClient(name = "localapp3", configuration = LocalRibbonClientConfiguration.class),
})
protected static class Application {
......@@ -215,11 +261,17 @@ public class FeignClientTests {
return ResponseEntity.ok().build();
}
@RequestMapping(method = RequestMethod.GET, value = "/fail")
String fail() {
throw new RuntimeException("always fails");
}
@RequestMapping(method = RequestMethod.GET, value = "/notFound")
ResponseEntity notFound() {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body((String)null);
}
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).properties(
"spring.application.name=feignclienttest",
......@@ -323,6 +375,42 @@ public class FeignClientTests {
assertNull("response body was not null", response.getBody());
}
@Test
public void testHystrixFallbackWorks() {
Hello hello = hystrixClient.fail();
assertNotNull("hello was null", hello);
assertEquals("message was wrong", "fallback", hello.getMessage());
}
@Test
@Ignore("Until HystrixCommand works in fallback")
public void testHystrixFallbackCommand() {
HystrixCommand<Hello> command = hystrixClient.failCommand();
assertNotNull("command was null", command);
Hello hello = command.execute();
assertNotNull("hello was null", hello);
assertEquals("message was wrong", "fallbackcommand", hello.getMessage());
}
@Test
@Ignore("Until Observable works in fallback")
public void testHystrixFallbackObservable() {
Observable<Hello> observable = hystrixClient.failObservable();
assertNotNull("observable was null", observable);
Hello hello = observable.toBlocking().first();
assertNotNull("hello was null", hello);
assertEquals("message was wrong", "fallbackobservable", hello.getMessage());
}
@Test
public void testHystrixFallbackFuture() throws Exception {
Future<Hello> future = hystrixClient.failFuture();
assertNotNull("future was null", future);
Hello hello = future.get(1, TimeUnit.SECONDS);
assertNotNull("hello was null", hello);
assertEquals("message was wrong", "fallbackfuture", hello.getMessage());
}
@Data
@AllArgsConstructor
@NoArgsConstructor
......@@ -336,6 +424,11 @@ public class FeignClientTests {
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public HystrixClientFallback hystrixClientFallback() {
return new HystrixClientFallback();
}
}
// Load balancer with fixed server list for "local" pointing to localhost
......
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