8000 Adds Hystrix SetterFactory to customize group and command keys by codefromthecrypt · Pull Request #447 · OpenFeign/feign · GitHub
[go: up one dir, main page]

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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
### Version 9.2
* Adds Hystrix `SetterFactory` to customize group and command keys
* Supports context path when using Ribbon `LoadBalancingTarget`
* Adds builder methods for the Response object
* Deprecates Response factory methods
Expand Down
30 changes: 29 additions & 1 deletion hystrix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,34 @@ api.getYourType("a").execute();
api.getYourTypeSynchronous("a");
```

### Group and Command keys

By default, Hystrix group keys match the target name, and the target name is usually the base url.
Hystrix command keys are the same as logging keys, which are equivalent to javadoc references.

For example, for the canonical GitHub example...

* the group key would be "https://api.github.com" and
* the command key would be "GitHub#contributors(String,String)"

You can use `HystrixFeign.Builder#setterFactory(SetterFactory)` to customize this, for example, to
read key mappings from configuration or annotations.

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)
...
```

### Fallback support

Fallbacks are known values, which you return when there's an error invoking an http method.
Expand Down Expand Up @@ -77,4 +105,4 @@ GitHub fallback = (owner, repo) -> {
GitHub github = HystrixFeign.builder()
...
.target(GitHub.class, "https://api.github.com", fallback);
```
```
11 changes: 10 additions & 1 deletion hystrix/src/main/java/feign/hystrix/HystrixFeign.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ public static Builder builder() {
public static final class Builder extends Feign.Builder {

private Contract contract = new Contract.Default();
private SetterFactory setterFactory = new SetterFactory.Default();

/**
* Allows you to override hystrix properties such as thread pools and command keys.
*/
public Builder setterFactory(SetterFactory setterFactory) {
this.setterFactory = setterFactory;
return this;
}

/**
* @see #target(Class, String, Object)
Expand Down Expand Up @@ -101,7 +110,7 @@ Feign buildWithFallback(final Object nullableFallback) {
super.invocationHandlerFactory(new InvocationHandlerFactory() {
@Override public InvocationHandler create(Target target,
Map<Method, MethodHandler> dispatch) {
return new HystrixInvocationHandler(target, dispatch, nullableFallback);
return new HystrixInvocationHandler(target, dispatch, setterFactory, nullableFallback);
}
});
super.contract(new HystrixDelegatingContract(contract));
Expand Down
43 changes: 27 additions & 16 deletions hystrix/src/main/java/feign/hystrix/HystrixInvocationHandler.java
8000
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
package feign.hystrix;

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

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import feign.InvocationHandlerFactory.MethodHandler;
import feign.Target;
Expand All @@ -40,23 +40,27 @@ final class HystrixInvocationHandler implements InvocationHandler {
private final Map<Method, MethodHandler> dispatch;
private final Object fallback; // Nullable
private final Map<Method, Method> fallbackMethodMap;
private final Map<Method, Setter> setterMethodMap;

HystrixInvocationHandler(Target<?> target, Map<Method, MethodHandler> dispatch, Object fallback) {
HystrixInvocationHandler(Target<?> target, Map<Method, MethodHandler> dispatch,
SetterFactory setterFactory, Object fallback) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallback = fallback;
this.fallbackMethodMap = toFallbackMethod(dispatch);
this.setterMethodMap = toSetters(setterFactory, target, dispatch.keySet());
}

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

/**
* Process all methods in the target so that appropriate setters are created.
*/
static Map<Method, Setter> toSetters(SetterFactory setterFactory, Target<?> target,
Set<Method> methods) {
Map<Method, Setter> result = new LinkedHashMap<Method, Setter>();
for (Method method : methods) {
method.setAccessible(true);
result.put(method, setterFactory.create(target, method));
}
return result;
}

@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
Expand All @@ -84,13 +101,7 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg
return toString();
}

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) {
HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setterMethodMap.get(method)) {
@Override
protected Object run() throws Exception {
try {
Expand Down Expand Up @@ -141,7 +152,7 @@ protected Object getFallback() {
} else if (isReturnsSingle(method)) {
// Create a cold Observable as a Single
return hystrixCommand.toObservable().toSingle();
} else if(isReturnsCompletable(method)) {
} else if (isReturnsCompletable(method)) {
return hystrixCommand.toObservable().toCompletable();
}
return hystrixCommand.execute();
Expand Down
44 changes: 44 additions & 0 deletions hystrix/src/main/java/feign/hystrix/SetterFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package feign.hystrix;

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

import java.lang.reflect.Method;

import feign.Feign;
import feign.Target;

/**
* Used to control properties of a hystrix command. Use cases include reading from static
* configuration or custom annotations.
*
* <p>This is parsed up-front, like {@link feign.Contract}, so will not be invoked for each command
* invocation.
*
* <p>Note: when deciding the {@link com.netflix.hystrix.HystrixCommand.Setter#andCommandKey(HystrixCommandKey)
* command key}, recall it lives in a shared cache, so make sure it is unique.
*/
public interface SetterFactory {

/**
* Returns a hystrix setter appropriate for the given target and method
*/
HystrixCommand.Setter create(Target<?> target, Method method);

/**
* Default behavior is to derive the group key from {@link Target#name()} and the command key from
* {@link Feign#configKey(Class, Method)}.
*/
final class Default implements SetterFactory {

@Override
public HystrixCommand.Setter create(Target<?> target, Method method) {
String groupKey = target.name();
String commandKey = Feign.configKey(target.type(), method);
return HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
}
}
}
6 changes: 3 additions & 3 deletions hystrix/src/test/java/feign/hystrix/HystrixBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public List<String> contributors(String owner, String repo) {
@Test
public void errorInFallbackHasExpectedBehavior() {
thrown.expect(HystrixRuntimeException.class);
thrown.expectMessage("contributors failed and fallback failed.");
thrown.expectMessage("GitHub#contributors(String,String) failed and fallback failed.");
thrown.expectCause(
isA(FeignException.class)); // as opposed to RuntimeException (from the fallback)

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

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

@Test
Expand Down
49 changes: 49 additions & 0 deletions hystrix/src/test/java/feign/hystrix/SetterFactoryTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package feign.hystrix;

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

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

import feign.RequestLine;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;

public class SetterFactoryTest {

interface TestInterface {
@RequestLine("POST /")
String invoke();
}

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

@Test
public void customSetter() {
thrown.expect(HystrixRuntimeException.class);
thrown.expectMessage("POST / failed and no fallback available.");

server.enqueue(new MockResponse().setResponseCode(500));

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));
};

TestInterface api = HystrixFeign.builder()
.setterFactory(commandKeyIsRequestLine)
.target(TestInterface.class, "http://localhost:" + server.getPort());

api.invoke();
}
}
0