Skip to content

Commit c17756c

Browse files
committed
add HystrixCommand support
fixes OpenFeigngh-189
1 parent 46e8911 commit c17756c

10 files changed

Lines changed: 327 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### Version 8.11
2+
* Adds support for Hystrix via a `HystrixFeign` builder.
3+
14
### Version 8.10
25
* Adds HTTP status to FeignException for easier response handling
36
* Reads class-level @Produces/@Consumes JAX-RS annotations

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,16 @@ MyService api = Feign.builder().client(RibbonClient.create()).target(MyService.c
152152

153153
```
154154

155+
### Hystrix
156+
[HystrixFeign](https://github.com/Netflix/feign/tree/master/hystrix) configures circuit breaker support provided by [Hystrix](https://github.com/Netflix/Hystrix).
157+
158+
To use Hystrix with Feign, add the Hystrix module to your classpath. Then use the `HystrixFeign` builder:
159+
160+
```java
161+
MyService api = HystrixFeign.builder().target(MyService.class, "https://myAppProd");
162+
163+
```
164+
155165
### SLF4J
156166
[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.)
157167

core/src/main/java/feign/MethodMetadata.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public String configKey() {
5050
return configKey;
5151
}
5252

53-
MethodMetadata configKey(String configKey) {
53+
public MethodMetadata configKey(String configKey) {
5454
this.configKey = configKey;
5555
return this;
5656
}
@@ -59,7 +59,7 @@ public Type returnType() {
5959
return returnType;
6060
}
6161

62-
MethodMetadata returnType(Type returnType) {
62+
public MethodMetadata returnType(Type returnType) {
6363
this.returnType = returnType;
6464
return this;
6565
}
@@ -68,7 +68,7 @@ public Integer urlIndex() {
6868
return urlIndex;
6969
}
7070

71-
MethodMetadata urlIndex(Integer urlIndex) {
71+
public MethodMetadata urlIndex(Integer urlIndex) {
7272
this.urlIndex = urlIndex;
7373
return this;
7474
}
@@ -77,7 +77,7 @@ public Integer bodyIndex() {
7777
return bodyIndex;
7878
}
7979

80-
MethodMetadata bodyIndex(Integer bodyIndex) {
80+
public MethodMetadata bodyIndex(Integer bodyIndex) {
8181
this.bodyIndex = bodyIndex;
8282
return this;
8383
}
@@ -89,7 +89,7 @@ public Type bodyType() {
8989
return bodyType;
9090
}
9191

92-
MethodMetadata bodyType(Type bodyType) {
92+
public MethodMetadata bodyType(Type bodyType) {
9393
this.bodyType = bodyType;
9494
return this;
9595
}

hystrix/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Hystrix
2+
===================
3+
4+
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).
5+
6+
To use Hystrix with Feign, add the Hystrix module to your classpath. Then, configure Feign to use the `HystrixInvocationHandler`:
7+
8+
```java
9+
GitHub github = HystrixFeign.builder()
10+
.target(GitHub.class, "https://api.github.com");
11+
```
12+
13+
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.
14+
15+
For asynchronous or reactive use, return `HystrixCommand<YourType>` rather than just `YourType`.
16+
17+
```java
18+
interface YourApi {
19+
@RequestLine("GET /yourtype/{id}")
20+
HystrixCommand<YourType> getYourType(@Param("id") String id);
21+
22+
@RequestLine("GET /yourtype/{id}")
23+
YourType getYourTypeSynchronous(@Param("id") String id);
24+
}
25+
26+
YourApi api = HystrixFeign.builder()
27+
.target(YourApi.class, "https://example.com");
28+
29+
// for reactive
30+
api.getYourType("a").toObservable();
31+
32+
// for asynchronous
33+
api.getYourType("a").queue();
34+
35+
// for synchronous
36+
api.getYourType("a").execute();
37+
38+
// or to apply hystrix to existing feign methods.
39+
api.getYourTypeSynchronous("a");
40+
```

hystrix/build.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apply plugin: 'java'
2+
3+
sourceCompatibility = 1.6
4+
5+
dependencies {
6+
compile project(':feign-core')
7+
compile 'com.netflix.hystrix:hystrix-core:1.4.18'
8+
testCompile 'junit:junit:4.12'
9+
testCompile 'org.assertj:assertj-core:1.7.1' // last version supporting JDK 7
10+
testCompile 'com.squareup.okhttp:mockwebserver:2.5.0'
11+
testCompile project(':feign-gson')
12+
testCompile project(':feign-core').sourceSets.test.output // for assertions
13+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package feign.hystrix;
2+
3+
import static feign.Util.resolveLastTypeParameter;
4+
5+
import java.lang.reflect.ParameterizedType;
6+
import java.lang.reflect.Type;
7+
import java.util.List;
8+
9+
import com.netflix.hystrix.HystrixCommand;
10+
11+
import feign.Contract;
12+
import feign.MethodMetadata;
13+
14+
final class HystrixDelegatingContract implements Contract {
15+
16+
private final Contract delegate;
17+
18+
public HystrixDelegatingContract(Contract delegate) {
19+
this.delegate = delegate;
20+
}
21+
22+
@Override
23+
public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
24+
List<MethodMetadata> metadatas = this.delegate.parseAndValidatateMetadata(targetType);
25+
26+
for (MethodMetadata metadata : metadatas) {
27+
Type type = metadata.returnType();
28+
29+
if (type instanceof ParameterizedType && ((ParameterizedType) type).getRawType().equals(HystrixCommand.class)) {
30+
Type actualType = resolveLastTypeParameter(type, HystrixCommand.class);
31+
metadata.returnType(actualType);
32+
}
33+
}
34+
35+
return metadatas;
36+
}
37+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package feign.hystrix;
2+
3+
import com.netflix.hystrix.HystrixCommand;
4+
5+
import feign.Contract;
6+
import feign.Feign;
7+
8+
/**
9+
* Allows Feign interfaces to return HystrixCommand objects.
10+
* Also decorates normal Feign methods with circuit breakers, but calls {@link HystrixCommand#execute()} directly.
11+
*/
12+
public final class HystrixFeign {
13+
14+
public static Builder builder() {
15+
return new Builder();
16+
}
17+
18+
public static final class Builder extends Feign.Builder {
19+
20+
public Builder() {
21+
invocationHandlerFactory(new HystrixInvocationHandler.Factory());
22+
contract(new HystrixDelegatingContract(new Contract.Default()));
23+
}
24+
25+
@Override
26+
public Feign.Builder contract(Contract contract) {
27+
return super.contract(new HystrixDelegatingContract(contract));
28+
}
29+
}
30+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2015 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign.hystrix;
17+
18+
import static feign.Util.checkNotNull;
19+
20+
import java.lang.reflect.InvocationHandler;
21+
import java.lang.reflect.Method;
22+
import java.util.Map;
23+
24+
import com.netflix.hystrix.HystrixCommand;
25+
import com.netflix.hystrix.HystrixCommandGroupKey;
26+
import com.netflix.hystrix.HystrixCommandKey;
27+
28+
import feign.InvocationHandlerFactory;
29+
import feign.InvocationHandlerFactory.MethodHandler;
30+
import feign.Target;
31+
32+
final class HystrixInvocationHandler implements InvocationHandler {
33+
34+
private final Target target;
35+
private final Map<Method, MethodHandler> dispatch;
36+
37+
HystrixInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
38+
this.target = checkNotNull(target, "target");
39+
this.dispatch = checkNotNull(dispatch, "dispatch");
40+
}
41+
42+
@Override
43+
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
44+
String groupKey = this.target.name();
45+
String commandKey = method.getName();
46+
HystrixCommand.Setter setter = HystrixCommand.Setter
47+
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
48+
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
49+
50+
HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setter) {
51+
@Override
52+
protected Object run() throws Exception {
53+
try {
54+
return HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
55+
} catch (Exception e) {
56+
throw e;
57+
} catch (Throwable t) {
58+
throw (Error)t;
59+
}
60+
}
61+
};
62+
63+
if (HystrixCommand.class.isAssignableFrom(method.getReturnType())) {
64+
return hystrixCommand;
65+
}
66+
return hystrixCommand.execute();
67+
}
68+
69+
static final class Factory implements InvocationHandlerFactory {
70+
71+
@Override
72+
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
73+
return new HystrixInvocationHandler(target, dispatch);
74+
}
75+
}
76+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package feign.hystrix;
2+
3+
import static feign.assertj.MockWebServerAssertions.assertThat;
4+
5+
import java.util.List;
6+
7+
import org.junit.Rule;
8+
import org.junit.Test;
9+
import org.junit.rules.ExpectedException;
10+
11+
import com.netflix.hystrix.HystrixCommand;
12+
import com.squareup.okhttp.mockwebserver.MockResponse;
13+
import com.squareup.okhttp.mockwebserver.MockWebServer;
14+
15+
import feign.Headers;
16+
import feign.RequestLine;
17+
import feign.gson.GsonDecoder;
18+
19+
public class HystrixBuilderTest {
20+
21+
@Rule
22+
public final ExpectedException thrown = ExpectedException.none();
23+
@Rule
24+
public final MockWebServer server = new MockWebServer();
25+
26+
@Test
27+
public void hystrixCommand() {
28+
server.enqueue(new MockResponse().setBody("\"foo\""));
29+
30+
TestInterface api = target();
31+
32+
HystrixCommand<String> command = api.command();
33+
34+
assertThat(command).isNotNull();
35+
assertThat(command.execute()).isEqualTo("foo");
36+
}
37+
38+
@Test
39+
public void hystrixCommandInt() {
40+
server.enqueue(new MockResponse().setBody("1"));
41+
42+
TestInterface api = target();
43+
44+
HystrixCommand<Integer> command = api.intCommand();
45+
46+
assertThat(command).isNotNull();
47+
assertThat(command.execute()).isEqualTo(new Integer(1));
48+
}
49+
50+
@Test
51+
public void hystrixCommandList() {
52+
server.enqueue(new MockResponse().setBody("[\"foo\",\"bar\"]"));
53+
54+
TestInterface api = target();
55+
56+
HystrixCommand<List<String>> command = api.listCommand();
57+
58+
assertThat(command).isNotNull();
59+
assertThat(command.execute()).hasSize(2).contains("foo", "bar");
60+
}
61+
62+
@Test
63+
public void plainString() {
64+
server.enqueue(new MockResponse().setBody("\"foo\""));
65+
66+
TestInterface api = target();
67+
68+
String string = api.get();
69+
70+
assertThat(string).isEqualTo("foo");
71+
}
72+
73+
@Test
74+
public void plainList() {
75+
server.enqueue(new MockResponse().setBody("[\"foo\",\"bar\"]"));
76+
77+
TestInterface api = target();
78+
79+
List<String> list = api.getList();
80+
81+
assertThat(list).isNotNull().hasSize(2).contains("foo", "bar");
82+
}
83+
84+
private TestInterface target() {
85+
return HystrixFeign.builder()
86+
.decoder(new GsonDecoder())
87+
.target(TestInterface.class, "http://localhost:" + server.getPort());
88+
}
89+
90+
interface TestInterface {
91+
92+
@RequestLine("GET /")
93+
@Headers("Accept: application/json")
94+
HystrixCommand<List<String>> listCommand();
95+
96+
@RequestLine("GET /")
97+
@Headers("Accept: application/json")
98+
HystrixCommand<String> command();
99+
100+
@RequestLine("GET /")
101+
@Headers("Accept: application/json")
102+
HystrixCommand<Integer> intCommand();
103+
104+
@RequestLine("GET /")
105+
@Headers("Accept: application/json")
106+
String get();
107+
108+
@RequestLine("GET /")
109+
@Headers("Accept: application/json")
110+
List<String> getList();
111+
}
112+
}

settings.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
rootProject.name='feign'
2-
include 'core', 'sax', 'gson', 'httpclient', 'jackson', 'jaxb', 'jaxrs', 'okhttp', 'ribbon', 'slf4j', 'jackson-jaxb'
2+
include 'core', 'sax', 'gson', 'httpclient', 'jackson', 'jaxb', 'jaxrs', 'okhttp', 'ribbon', 'slf4j', 'jackson-jaxb', 'hystrix'
33

44
rootProject.children.each { childProject ->
55
childProject.name = 'feign-' + childProject.name

0 commit comments

Comments
 (0)