Skip to content

Commit 177ce5e

Browse files
authored
Adds Hystrix SetterFactory to customize group and command keys (OpenFeign#447)
This exposes means to customize group and command keys, for example to use non-default conventions from configuration or custom annotation processing. Ex. ```java SetterFactory commandKeyIsRequestLine = (target, method) -> { String groupKey = target.name(); String commandKey = method.getAnnotation(RequestLine.class).value(); return HystrixCommand.Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey)) .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey)); }; api = HystrixFeign.builder() .setterFactory(commandKeyIsRequestLine) ... ``` This also makes the default's more unique to avoid clashing in Hystrix's cache.
1 parent b6bf1ca commit 177ce5e

File tree

7 files changed

+163
-21
lines changed

7 files changed

+163
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
### Version 9.2
2+
* Adds Hystrix `SetterFactory` to customize group and command keys
23
* Supports context path when using Ribbon `LoadBalancingTarget`
34
* Adds builder methods for the Response object
45
* Deprecates Response factory methods

hystrix/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,34 @@ api.getYourType("a").execute();
5050
api.getYourTypeSynchronous("a");
5151
```
5252

53+
### Group and Command keys
54+
55+
By default, Hystrix group keys match the target name, and the target name is usually the base url.
56+
Hystrix command keys are the same as logging keys, which are equivalent to javadoc references.
57+
58+
For example, for the canonical GitHub example...
59+
60+
* the group key would be "https://api.github.com" and
61+
* the command key would be "GitHub#contributors(String,String)"
62+
63+
You can use `HystrixFeign.Builder#setterFactory(SetterFactory)` to customize this, for example, to
64+
read key mappings from configuration or annotations.
65+
66+
Ex.
67+
```java
68+
SetterFactory commandKeyIsRequestLine = (target, method) -> {
69+
String groupKey = target.name();
70+
String commandKey = method.getAnnotation(RequestLine.class).value();
71+
return HystrixCommand.Setter
72+
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
73+
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
74+
};
75+
76+
api = HystrixFeign.builder()
77+
.setterFactory(commandKeyIsRequestLine)
78+
...
79+
```
80+
5381
### Fallback support
5482

5583
Fallbacks are known values, which you return when there's an error invoking an http method.
@@ -77,4 +105,4 @@ GitHub fallback = (owner, repo) -> {
77105
GitHub github = HystrixFeign.builder()
78106
...
79107
.target(GitHub.class, "https://api.github.com", fallback);
80-
```
108+
```

hystrix/src/main/java/feign/hystrix/HystrixFeign.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ public static Builder builder() {
3333
public static final class Builder extends Feign.Builder {
3434

3535
private Contract contract = new Contract.Default();
36+
private SetterFactory setterFactory = new SetterFactory.Default();
37+
38+
/**
39+
* Allows you to override hystrix properties such as thread pools and command keys.
40+
*/
41+
public Builder setterFactory(SetterFactory setterFactory) {
42+
this.setterFactory = setterFactory;
43+
return this;
44+
}
3645

3746
/**
3847
* @see #target(Class, String, Object)
@@ -101,7 +110,7 @@ Feign buildWithFallback(final Object nullableFallback) {
101110
super.invocationHandlerFactory(new InvocationHandlerFactory() {
102111
@Override public InvocationHandler create(Target target,
103112
Map<Method, MethodHandler> dispatch) {
104-
return new HystrixInvocationHandler(target, dispatch, nullableFallback);
113+
return new HystrixInvocationHandler(target, dispatch, setterFactory, nullableFallback);
105114
}
106115
});
107116
super.contract(new HystrixDelegatingContract(contract));

hystrix/src/main/java/feign/hystrix/HystrixInvocationHandler.java

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616
package feign.hystrix;
1717

1818
import com.netflix.hystrix.HystrixCommand;
19-
import com.netflix.hystrix.HystrixCommandGroupKey;
20-
import com.netflix.hystrix.HystrixCommandKey;
19+
import com.netflix.hystrix.HystrixCommand.Setter;
2120

2221
import java.lang.reflect.InvocationHandler;
2322
import java.lang.reflect.InvocationTargetException;
2423
import java.lang.reflect.Method;
2524
import java.lang.reflect.Proxy;
2625
import java.util.LinkedHashMap;
2726
import java.util.Map;
27+
import java.util.Set;
2828

2929
import feign.InvocationHandlerFactory.MethodHandler;
3030
import feign.Target;
@@ -40,23 +40,27 @@ final class HystrixInvocationHandler implements InvocationHandler {
4040
private final Map<Method, MethodHandler> dispatch;
4141
private final Object fallback; // Nullable
4242
private final Map<Method, Method> fallbackMethodMap;
43+
private final Map<Method, Setter> setterMethodMap;
4344

44-
HystrixInvocationHandler(Target<?> target, Map<Method, MethodHandler> dispatch, Object fallback) {
45+
HystrixInvocationHandler(Target<?> target, Map<Method, MethodHandler> dispatch,
46+
SetterFactory setterFactory, Object fallback) {
4547
this.target = checkNotNull(target, "target");
4648
this.dispatch = checkNotNull(dispatch, "dispatch");
4749
this.fallback = fallback;
4850
this.fallbackMethodMap = toFallbackMethod(dispatch);
51+
this.setterMethodMap = toSetters(setterFactory, target, dispatch.keySet());
4952
}
5053

5154
/**
5255
* If the method param of InvocationHandler.invoke is not accessible, i.e in a package-private
53-
* interface, the fallback call in hystrix command will fail cause of access restrictions.
54-
* But methods in dispatch are copied methods. So setting access to dispatch method doesn't take
55-
* effect to the method in InvocationHandler.invoke. Use map to store a copy of method
56-
* to invoke the fallback to bypass this and reducing the count of reflection calls.
56+
* interface, the fallback call in hystrix command will fail cause of access restrictions. But
57+
* methods in dispatch are copied methods. So setting access to dispatch method doesn't take
58+
* effect to the method in InvocationHandler.invoke. Use map to store a copy of method to invoke
59+
* the fallback to bypass this and reducing the count of reflection calls.
60+
*
5761
* @return cached methods map for fallback invoking
5862
*/
59-
private Map<Method, Method> toFallbackMethod(Map<Method, MethodHandler> dispatch) {
63+
static Map<Method, Method> toFallbackMethod(Map<Method, MethodHandler> dispatch) {
6064
Map<Method, Method> result = new LinkedHashMap<Method, Method>();
6165
for (Method method : dispatch.keySet()) {
6266
method.setAccessible(true);
@@ -65,6 +69,19 @@ private Map<Method, Method> toFallbackMethod(Map<Method, MethodHandler> dispatch
6569
return result;
6670
}
6771

72+
/**
73+
* Process all methods in the target so that appropriate setters are created.
74+
*/
75+
static Map<Method, Setter> toSetters(SetterFactory setterFactory, Target<?> target,
76+
Set<Method> methods) {
77+
Map<Method, Setter> result = new LinkedHashMap<Method, Setter>();
78+
for (Method method : methods) {
79+
method.setAccessible(true);
80+
result.put(method, setterFactory.create(target, method));
81+
}
82+
return result;
83+
}
84+
6885
@Override
6986
public Object invoke(final Object proxy, final Method method, final Object[] args)
7087
throws Throwable {
@@ -84,13 +101,7 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg
84101
return toString();
85102
}
86103

87-
String groupKey = this.target.name();
88-
String commandKey = method.getName();
89-
HystrixCommand.Setter setter = HystrixCommand.Setter
90-
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
91-
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
92-
93-
HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setter) {
104+
HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setterMethodMap.get(method)) {
94105
@Override
95106
protected Object run() throws Exception {
96107
try {
@@ -141,7 +152,7 @@ protected Object getFallback() {
141152
} else if (isReturnsSingle(method)) {
142153
// Create a cold Observable as a Single
143154
return hystrixCommand.toObservable().toSingle();
144-
} else if(isReturnsCompletable(method)) {
155+
} else if (isReturnsCompletable(method)) {
145156
return hystrixCommand.toObservable().toCompletable();
146157
}
147158
return hystrixCommand.execute();
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package feign.hystrix;
2+
3+
import com.netflix.hystrix.HystrixCommand;
4+
import com.netflix.hystrix.HystrixCommandGroupKey;
5+
import com.netflix.hystrix.HystrixCommandKey;
6+
7+
import java.lang.reflect.Method;
8+
9+
import feign.Feign;
10+
import feign.Target;
11+
12+
/**
13+
* Used to control properties of a hystrix command. Use cases include reading from static
14+
* configuration or custom annotations.
15+
*
16+
* <p>This is parsed up-front, like {@link feign.Contract}, so will not be invoked for each command
17+
* invocation.
18+
*
19+
* <p>Note: when deciding the {@link com.netflix.hystrix.HystrixCommand.Setter#andCommandKey(HystrixCommandKey)
20+
* command key}, recall it lives in a shared cache, so make sure it is unique.
21+
*/
22+
public interface SetterFactory {
23+
24+
/**
25+
* Returns a hystrix setter appropriate for the given target and method
26+
*/
27+
HystrixCommand.Setter create(Target<?> target, Method method);
28+
29+
/**
30+
* Default behavior is to derive the group key from {@link Target#name()} and the command key from
31+
* {@link Feign#configKey(Class, Method)}.
32+
*/
33+
final class Default implements SetterFactory {
34+
35+
@Override
36+
public HystrixCommand.Setter create(Target<?> target, Method method) {
37+
String groupKey = target.name();
38+
String commandKey = Feign.configKey(target.type(), method);
39+
return HystrixCommand.Setter
40+
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
41+
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
42+
}
43+
}
44+
}

hystrix/src/test/java/feign/hystrix/HystrixBuilderTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public List<String> contributors(String owner, String repo) {
148148
@Test
149149
public void errorInFallbackHasExpectedBehavior() {
150150
thrown.expect(HystrixRuntimeException.class);
151-
thrown.expectMessage("contributors failed and fallback failed.");
151+
thrown.expectMessage("GitHub#contributors(String,String) failed and fallback failed.");
152152
thrown.expectCause(
153153
isA(FeignException.class)); // as opposed to RuntimeException (from the fallback)
154154

@@ -170,7 +170,7 @@ public List<String> contributors(String owner, String repo) {
170170
@Test
171171
public void hystrixRuntimeExceptionPropagatesOnException() {
172172
thrown.expect(HystrixRuntimeException.class);
173-
thrown.expectMessage("contributors failed and no fallback available.");
173+
thrown.expectMessage("GitHub#contributors(String,String) failed and no fallback available.");
174174
thrown.expectCause(isA(FeignException.class));
175175

176176
server.enqueue(new MockResponse().setResponseCode(500));
@@ -301,7 +301,7 @@ public void rxObservableListFall_noFallback() {
301301
assertThat(testSubscriber.getOnNextEvents()).isEmpty();
302302
assertThat(testSubscriber.getOnErrorEvents().get(0))
303303
.isInstanceOf(HystrixRuntimeException.class)
304-
.hasMessage("listObservable failed and no fallback available.");
304+
.hasMessage("TestInterface#listObservable() failed and no fallback available.");
305305
}
306306

307307
@Test
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package feign.hystrix;
2+
3+
import com.netflix.hystrix.HystrixCommand;
4+
import com.netflix.hystrix.HystrixCommandGroupKey;
5+
import com.netflix.hystrix.HystrixCommandKey;
6+
import com.netflix.hystrix.exception.HystrixRuntimeException;
7+
8+
import org.junit.Rule;
9+
import org.junit.Test;
10+
import org.junit.rules.ExpectedException;
11+
12+
import feign.RequestLine;
13+
import okhttp3.mockwebserver.MockResponse;
14+
import okhttp3.mockwebserver.MockWebServer;
15+
16+
public class SetterFactoryTest {
17+
18+
interface TestInterface {
19+
@RequestLine("POST /")
20+
String invoke();
21+
}
22+
23+
@Rule
24+
public final ExpectedException thrown = ExpectedException.none();
25+
@Rule
26+
public final MockWebServer server = new MockWebServer();
27+
28+
@Test
29+
public void customSetter() {
30+
thrown.expect(HystrixRuntimeException.class);
31+
thrown.expectMessage("POST / failed and no fallback available.");
32+
33+
server.enqueue(new MockResponse().setResponseCode(500));
34+
35+
SetterFactory commandKeyIsRequestLine = (target, method) -> {
36+
String groupKey = target.name();
37+
String commandKey = method.getAnnotation(RequestLine.class).value();
38+
return HystrixCommand.Setter
39+
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
40+
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
41+
};
42+
43+
TestInterface api = HystrixFeign.builder()
44+
.setterFactory(commandKeyIsRequestLine)
45+
.target(TestInterface.class, "http://localhost:" + server.getPort());
46+
47+
api.invoke();
48+
}
49+
}

0 commit comments

Comments
 (0)