Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
### Version 8.11
* Adds support for Hystrix via a `HystrixFeign` builder.

### Version 8.10
* Adds HTTP status to FeignException for easier response handling
* Reads class-level @Produces/@Consumes JAX-RS annotations
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ MyService api = Feign.builder().client(RibbonClient.create()).target(MyService.c

```

### Hystrix
[HystrixFeign](https://github.com/Netflix/feign/tree/master/hystrix) configures circuit breaker support provided by [Hystrix](https://github.com/Netflix/Hystrix).

To use Hystrix with Feign, add the Hystrix module to your classpath. Then use the `HystrixFeign` builder:

```java
MyService api = HystrixFeign.builder().target(MyService.class, "https://myAppProd");

```

### SLF4J
[SLF4JModule](https://github.com/Netflix/feign/tree/master/slf4j) allows directing Feign's logging to [SLF4J](http://www.slf4j.org/), allowing you to easily use a logging backend of your choice (Logback, Log4J, etc.)

Expand Down
10 changes: 5 additions & 5 deletions core/src/main/java/feign/MethodMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public String configKey() {
return configKey;
}

MethodMetadata configKey(String configKey) {
public MethodMetadata configKey(String configKey) {
this.configKey = configKey;
return this;
}
Expand All @@ -59,7 +59,7 @@ public Type returnType() {
return returnType;
}

MethodMetadata returnType(Type returnType) {
public MethodMetadata returnType(Type returnType) {
this.returnType = returnType;
return this;
}
Expand All @@ -68,7 +68,7 @@ public Integer urlIndex() {
return urlIndex;
}

MethodMetadata urlIndex(Integer urlIndex) {
public MethodMetadata urlIndex(Integer urlIndex) {
this.urlIndex = urlIndex;
return this;
}
Expand All @@ -77,7 +77,7 @@ public Integer bodyIndex() {
return bodyIndex;
}

MethodMetadata bodyIndex(Integer bodyIndex) {
public MethodMetadata bodyIndex(Integer bodyIndex) {
this.bodyIndex = bodyIndex;
return this;
}
Expand All @@ -89,7 +89,7 @@ public Type bodyType() {
return bodyType;
}

MethodMetadata bodyType(Type bodyType) {
public MethodMetadata bodyType(Type bodyType) {
this.bodyType = bodyType;
return this;
}
Expand Down
40 changes: 40 additions & 0 deletions hystrix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
Hystrix
===================

This module wraps Feign's http requests in [Hystrix](https://github.com/Netflix/Hystrix/), which enables the [Circuit Breaker Pattern](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern).

To use Hystrix with Feign, add the Hystrix module to your classpath. Then, configure Feign to use the `HystrixInvocationHandler`:

```java
GitHub github = HystrixFeign.builder()
.target(GitHub.class, "https://api.github.com");
```

Methods that do *not* return [`HystrixCommand`](https://netflix.github.io/Hystrix/javadoc/com/netflix/hystrix/HystrixCommand.html) are still wrapped in a `HystrixCommand`, but `execute()` is automatically called for you.

For asynchronous or reactive use, return `HystrixCommand<YourType>` rather than just `YourType`.

```java
interface YourApi {
@RequestLine("GET /yourtype/{id}")
HystrixCommand<YourType> getYourType(@Param("id") String id);

@RequestLine("GET /yourtype/{id}")
YourType getYourTypeSynchronous(@Param("id") String id);
}

YourApi api = HystrixFeign.builder()
.target(YourApi.class, "https://example.com");

// for reactive
api.getYourType("a").toObservable();

// for asynchronous
api.getYourType("a").queue();

// for synchronous
api.getYourType("a").execute();

// or to apply hystrix to existing feign methods.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

api.getYourTypeSynchronous("a");
```
13 changes: 13 additions & 0 deletions hystrix/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apply plugin: 'java'

sourceCompatibility = 1.6

dependencies {
compile project(':feign-core')
compile 'com.netflix.hystrix:hystrix-core:1.4.18'
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1' // last version supporting JDK 7
testCompile 'com.squareup.okhttp:mockwebserver:2.5.0'
testCompile project(':feign-gson')
testCompile project(':feign-core').sourceSets.test.output // for assertions
}
37 changes: 37 additions & 0 deletions hystrix/src/main/java/feign/hystrix/HystrixDelegatingContract.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package feign.hystrix;

import static feign.Util.resolveLastTypeParameter;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

import com.netflix.hystrix.HystrixCommand;

import feign.Contract;
import feign.MethodMetadata;

final class HystrixDelegatingContract implements Contract {

private final Contract delegate;

public HystrixDelegatingContract(Contract delegate) {
this.delegate = delegate;
}

@Override
public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
List<MethodMetadata> metadatas = this.delegate.parseAndValidatateMetadata(targetType);

for (MethodMetadata metadata : metadatas) {
Type type = metadata.returnType();

if (type instanceof ParameterizedType && ((ParameterizedType) type).getRawType().equals(HystrixCommand.class)) {
Type actualType = resolveLastTypeParameter(type, HystrixCommand.class);
metadata.returnType(actualType);
}
}

return metadatas;
}
}
30 changes: 30 additions & 0 deletions hystrix/src/main/java/feign/hystrix/HystrixFeign.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package feign.hystrix;

import com.netflix.hystrix.HystrixCommand;

import feign.Contract;
import feign.Feign;

/**
* Allows Feign interfaces to return HystrixCommand objects.
* Also decorates normal Feign methods with circuit breakers, but calls {@link HystrixCommand#execute()} directly.
*/
public final class HystrixFeign {

public static Builder builder() {
return new Builder();
}

public static final class Builder extends Feign.Builder {

public Builder() {
invocationHandlerFactory(new HystrixInvocationHandler.Factory());
contract(new HystrixDelegatingContract(new Contract.Default()));
}

@Override
public Feign.Builder contract(Contract contract) {
return super.contract(new HystrixDelegatingContract(contract));
}
}
}
76 changes: 76 additions & 0 deletions hystrix/src/main/java/feign/hystrix/HystrixInvocationHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign.hystrix;

import static feign.Util.checkNotNull;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;

import feign.InvocationHandlerFactory;
import feign.InvocationHandlerFactory.MethodHandler;
import feign.Target;

final class HystrixInvocationHandler implements InvocationHandler {

private final Target target;
private final Map<Method, MethodHandler> dispatch;

HystrixInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
}

@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
String groupKey = this.target.name();
String commandKey = method.getName();
HystrixCommand.Setter setter = HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));

HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setter) {
@Override
protected Object run() throws Exception {
try {
return HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
} catch (Exception e) {
throw e;
} catch (Throwable t) {
throw (Error)t;
}
}
};

if (HystrixCommand.class.isAssignableFrom(method.getReturnType())) {
return hystrixCommand;
}
return hystrixCommand.execute();
}

static final class Factory implements InvocationHandlerFactory {

@Override
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
return new HystrixInvocationHandler(target, dispatch);
}
}
}
112 changes: 112 additions & 0 deletions hystrix/src/test/java/feign/hystrix/HystrixBuilderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package feign.hystrix;

import static feign.assertj.MockWebServerAssertions.assertThat;

import java.util.List;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import com.netflix.hystrix.HystrixCommand;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;

import feign.Headers;
import feign.RequestLine;
import feign.gson.GsonDecoder;

public class HystrixBuilderTest {

@Rule
public final ExpectedException thrown = ExpectedException.none();
@Rule
public final MockWebServer server = new MockWebServer();

@Test
public void hystrixCommand() {
server.enqueue(new MockResponse().setBody("\"foo\""));

TestInterface api = target();

HystrixCommand<String> command = api.command();

assertThat(command).isNotNull();
assertThat(command.execute()).isEqualTo("foo");
}

@Test
public void hystrixCommandInt() {
server.enqueue(new MockResponse().setBody("1"));

TestInterface api = target();

HystrixCommand<Integer> command = api.intCommand();

assertThat(command).isNotNull();
assertThat(command.execute()).isEqualTo(new Integer(1));
}

@Test
public void hystrixCommandList() {
server.enqueue(new MockResponse().setBody("[\"foo\",\"bar\"]"));

TestInterface api = target();

HystrixCommand<List<String>> command = api.listCommand();

assertThat(command).isNotNull();
assertThat(command.execute()).hasSize(2).contains("foo", "bar");
}

@Test
public void plainString() {
server.enqueue(new MockResponse().setBody("\"foo\""));

TestInterface api = target();

String string = api.get();

assertThat(string).isEqualTo("foo");
}

@Test
public void plainList() {
server.enqueue(new MockResponse().setBody("[\"foo\",\"bar\"]"));

TestInterface api = target();

List<String> list = api.getList();

assertThat(list).isNotNull().hasSize(2).contains("foo", "bar");
}

private TestInterface target() {
return HystrixFeign.builder()
.decoder(new GsonDecoder())
.target(TestInterface.class, "http://localhost:" + server.getPort());
}

interface TestInterface {

@RequestLine("GET /")
@Headers("Accept: application/json")
HystrixCommand<List<String>> listCommand();

@RequestLine("GET /")
@Headers("Accept: application/json")
HystrixCommand<String> command();

@RequestLine("GET /")
@Headers("Accept: application/json")
HystrixCommand<Integer> intCommand();

@RequestLine("GET /")
@Headers("Accept: application/json")
String get();

@RequestLine("GET /")
@Headers("Accept: application/json")
List<String> getList();
}
}
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
rootProject.name='feign'
include 'core', 'sax', 'gson', 'httpclient', 'jackson', 'jaxb', 'jaxrs', 'okhttp', 'ribbon', 'slf4j', 'jackson-jaxb'
include 'core', 'sax', 'gson', 'httpclient', 'jackson', 'jaxb', 'jaxrs', 'okhttp', 'ribbon', 'slf4j', 'jackson-jaxb', 'hystrix'

rootProject.children.each { childProject ->
childProject.name = 'feign-' + childProject.name
Expand Down