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