diff --git a/libraries.gradle b/libraries.gradle index cfc7b61c69..ef21fc912d 100644 --- a/libraries.gradle +++ b/libraries.gradle @@ -21,6 +21,7 @@ ext { ratpackVersion = '1.5.4' spockVersion = '1.1-groovy-2.4-rc-4' retrofitVersion = '2.3.0' + feignVersion = '10.0.1' prometheusSimpleClientVersion = '0.3.0' reactorVersion = '3.1.5.RELEASE' reactiveStreamsVersion = '1.0.2' @@ -86,7 +87,11 @@ ext { retrofit: "com.squareup.retrofit2:retrofit:${retrofitVersion}", retrofit_test: "com.squareup.retrofit2:converter-scalars:${retrofitVersion}", retrofit_wiremock: "com.github.tomakehurst:wiremock:1.58", - + + // Feign addon + feign: "io.github.openfeign:feign-core:${feignVersion}", + feign_wiremock: "com.github.tomakehurst:wiremock:1.58", + // Metrics addon metrics: "io.dropwizard.metrics:metrics-core:${metricsVersion}", diff --git a/resilience4j-feign/README.adoc b/resilience4j-feign/README.adoc new file mode 100644 index 0000000000..0a040b8385 --- /dev/null +++ b/resilience4j-feign/README.adoc @@ -0,0 +1,104 @@ += resilience4j-feign + +Resilience4j decorators for https://github.com/OpenFeign/feign[feign]. +Similar to https://github.com/OpenFeign/feign/tree/master/hystrix[HystrixFeign], +resilience4j-feign makes it easy to incorporate "fault tolerance" patterns into the feign framework, such as + the CircuitBreaker and RateLimiter. + + +== Current Features +* CircuitBreaker +* RateLimiter +* Fallback + + +== Decorating Feign Interfaces + +The `Resilience4jFeign.builder` is the main class for creating fault tolerance instances of feign. +It extends the `Feign.builder` and can be configured in the same way with the exception of adding a custom +`InvocationHandlerFactory`. Resilience4jFeign uses its own `InvocationHandlerFactory` to apply the decorators. +Decorators can be built using the `FeignDecorators` class. Multiple decorators can be combined + +The following example shows how to decorate a feign interface with a RateLimiter and CircuitBreaker: +``` java + public interface MyService { + @RequestLine("GET /greeting") + String getGreeting(); + + @RequestLine("POST /greeting") + String createGreeting(); + } + + CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendName"); + RateLimiter rateLimiter = RateLimiter.ofDefaults("backendName"); + FeignDecorators decorators = FeignDecorators.builder() + .withRateLimiter(rateLimiter) + .withCircuitBreaker(circuitBreaker) + .build(); + MyService myService = Resilience4jFeign.builder(decorators).target(MyService.class, "http://localhost:8080/"); +``` + +Calling any method of the `MyService` instance will invoke a CircuitBreaker and then a RateLimiter. +If one of these mechanisms take affect, then the corresponding RuntimeException will be thrown, for example, `CircuitBreakerOpenException` or `RequestNotPermitted` (Hint: These do not extend the `FeignException` class). + +The following diagram illustrates how the decorators are stacked: +image::feign-decorators.png[] + + +== Ordering of Decorators +The order in which decorators are applied correspond to the order in which they are declared. +When building `FeignDecorators`, it is important to be wary of this, since the order affects the resulting behavior. + +For example, +``` java + FeignDecorators decoratorsA = FeignDecorators.builder() + .withCircuitBreaker(circuitBreaker) + .withRateLimiter(rateLimiter) + .build(); + + FeignDecorators decoratorsB = FeignDecorators.builder() + .withRateLimiter(rateLimiter) + .withCircuitBreaker(circuitBreaker) + .build(); +``` + +With `decoratorsA` the RateLimiter will be called before the CircuitBreaker. That means that even if the CircuitBreaker is open, the RateLimiter will still limit the rate of calls. +`decoratorsB` applies the reserve order. Meaning that once the CircuitBreaker is open, the RateLimiter will no longer be in affect. + + +== Fallback +Fallbacks can be defined that are called when Exceptions are thrown. Exceptions can occur when the HTTP request fails, but also when one of the `FeignDecorators` activates, for example, the CircuitBreaker. + +``` java + public interface MyService { + @RequestLine("GET /greeting") + String greeting(); + } + + MyService requestFailedFallback = () -> "fallback greeting"; + MyService circuitBreakerFallback = () -> "CircuitBreaker is open!"; + CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendName"); + FeignDecorators decorators = FeignDecorators.builder() + .withFallback(requestFailedFallback, FeignException.class) + .withFallback(circuitBreakerFallback, CircuitBreakerOpenException.class) + .build(); + MyService myService = Resilience4jFeign.builder(decorators).target(MyService.class, "http://localhost:8080/", fallback); +``` +In this example, the `requestFailedFallback` is called when a `FeignException` is thrown (usually when the HTTP request fails), whereas + the `circuitBreakerFallback` is only called in the case of a `CircuitBreakerOpenException`. + Check the `FeignDecorators` class for more ways to filter fallbacks. + +All fallbacks must implement the same interface that is declared in the "target" (Resilience4jFeign.Builder#target) method, otherwise an IllegalArgumentException will be thrown. +Multiple fallbacks can be assigned to handle the same Exception with the next fallback being called when the previous one fails. + + + +== License + +Copyright 2018 + +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. \ No newline at end of file diff --git a/resilience4j-feign/build.gradle b/resilience4j-feign/build.gradle new file mode 100644 index 0000000000..e8595dbbbd --- /dev/null +++ b/resilience4j-feign/build.gradle @@ -0,0 +1,6 @@ +dependencies { + compile ( libraries.feign ) + compile project(':resilience4j-circuitbreaker') + compile project(':resilience4j-ratelimiter') + testCompile ( libraries.feign_wiremock ) +} \ No newline at end of file diff --git a/resilience4j-feign/feign-decorators.png b/resilience4j-feign/feign-decorators.png new file mode 100644 index 0000000000..19c6c0fda6 Binary files /dev/null and b/resilience4j-feign/feign-decorators.png differ diff --git a/resilience4j-feign/src/main/java/io/github/resilience4j/feign/DecoratorInvocationHandler.java b/resilience4j-feign/src/main/java/io/github/resilience4j/feign/DecoratorInvocationHandler.java new file mode 100644 index 0000000000..c5a887fabb --- /dev/null +++ b/resilience4j-feign/src/main/java/io/github/resilience4j/feign/DecoratorInvocationHandler.java @@ -0,0 +1,117 @@ +/* + * + * Copyright 2018 + * + * 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 io.github.resilience4j.feign; + +import static feign.Util.checkNotNull; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; + +import feign.InvocationHandlerFactory.MethodHandler; +import feign.Target; +import io.vavr.CheckedFunction1; + +/** + * An instance of {@link InvocationHandler} that uses {@link FeignDecorator}s to enhance the + * invocations of methods. + */ +class DecoratorInvocationHandler implements InvocationHandler { + + private final Target target; + private final Map> decoratedDispatch; + + public DecoratorInvocationHandler(Target target, + Map dispatch, + FeignDecorator invocationDecorator) { + this.target = checkNotNull(target, "target"); + checkNotNull(dispatch, "dispatch"); + this.decoratedDispatch = decorateMethodHandlers(dispatch, invocationDecorator, target); + } + + /** + * Applies the specified {@link FeignDecorator} to all specified {@link MethodHandler}s and + * returns the result as a map of {@link CheckedFunction1}s. Invoking a {@link CheckedFunction1} + * will therefore invoke the decorator which, in turn, may invoke the corresponding + * {@link MethodHandler}. + * + * @param dispatch a map of the methods from the feign interface to the {@link MethodHandler}s. + * @param invocationDecorator the {@link FeignDecorator} with which to decorate the + * {@link MethodHandler}s. + * @param target the target feign interface. + * @return a new map where the {@link MethodHandler}s are decorated with the + * {@link FeignDecorator}. + */ + private Map> decorateMethodHandlers(Map dispatch, + FeignDecorator invocationDecorator, Target target) { + final Map> map = new HashMap<>(); + for (final Map.Entry entry : dispatch.entrySet()) { + final Method method = entry.getKey(); + final MethodHandler methodHandler = entry.getValue(); + map.put(method, invocationDecorator.decorate(methodHandler::invoke, method, methodHandler, target)); + } + return map; + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) + throws Throwable { + switch (method.getName()) { + case "equals": + return equals(args.length > 0 ? args[0] : null); + + case "hashCode": + return hashCode(); + + case "toString": + return toString(); + + default: + break; + } + + return decoratedDispatch.get(method).apply(args); + } + + @Override + public boolean equals(Object obj) { + Object compareTo = obj; + if (compareTo == null) { + return false; + } + if (Proxy.isProxyClass(compareTo.getClass())) { + compareTo = Proxy.getInvocationHandler(compareTo); + } + if (compareTo instanceof DecoratorInvocationHandler) { + final DecoratorInvocationHandler other = (DecoratorInvocationHandler) compareTo; + return target.equals(other.target); + } + return false; + } + + @Override + public int hashCode() { + return target.hashCode(); + } + + @Override + public String toString() { + return target.toString(); + } +} diff --git a/resilience4j-feign/src/main/java/io/github/resilience4j/feign/FallbackDecorator.java b/resilience4j-feign/src/main/java/io/github/resilience4j/feign/FallbackDecorator.java new file mode 100644 index 0000000000..37a3ee530a --- /dev/null +++ b/resilience4j-feign/src/main/java/io/github/resilience4j/feign/FallbackDecorator.java @@ -0,0 +1,106 @@ +/* + * + * Copyright 2018 + * + * 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 io.github.resilience4j.feign; + +import static java.util.Objects.requireNonNull; + +import java.lang.reflect.Method; +import java.util.function.Predicate; + +import feign.InvocationHandlerFactory.MethodHandler; +import feign.Target; +import io.vavr.CheckedFunction1; + +/** + * Decorator that calls a fallback in the case that an exception is thrown. + */ +class FallbackDecorator implements FeignDecorator { + + private final T fallback; + private Predicate filter; + + /** + * Creates a fallback that will be called for every {@link Exception}. + */ + public FallbackDecorator(T fallback) { + this(fallback, ex -> true); + } + + /** + * Creates a fallback that will only be called for the specified {@link Exception}. + */ + public FallbackDecorator(T fallback, Class filter) { + this(fallback, filter::isInstance); + requireNonNull(filter, "Filter cannot be null!"); + } + + /** + * Creates a fallback that will only be called if the specified {@link Predicate} returns + * true. + */ + public FallbackDecorator(T fallback, Predicate filter) { + this.fallback = requireNonNull(fallback, "Fallback cannot be null!"); + this.filter = requireNonNull(filter, "Filter cannot be null!"); + } + + /** + * Calls the fallback if the invocationCall throws an {@link Exception}. + * + * @throws IllegalArgumentException if the fallback object does not have a corresponding + * fallback method. + */ + @Override + public CheckedFunction1 decorate(CheckedFunction1 invocationCall, + Method method, + MethodHandler methodHandler, + Target target) { + final Method fallbackMethod; + validateFallback(method); + fallbackMethod = getFallbackMethod(method); + return args -> { + try { + return invocationCall.apply(args); + } catch (final Exception exception) { + if (filter.test(exception)) { + return fallbackMethod.invoke(fallback, args); + } + throw exception; + } + }; + } + + private void validateFallback(Method method) { + if (fallback.getClass().isAssignableFrom(method.getDeclaringClass())) { + throw new IllegalArgumentException("Cannot use the fallback [" + + fallback.getClass() + "] for [" + + method.getDeclaringClass() + "]!"); + } + } + + private Method getFallbackMethod(Method method) { + Method fallbackMethod; + try { + fallbackMethod = fallback.getClass().getMethod(method.getName(), method.getParameterTypes()); + } catch (NoSuchMethodException | SecurityException e) { + throw new IllegalArgumentException("Cannot use the fallback [" + + fallback.getClass() + "] for [" + + method.getDeclaringClass() + "]", e); + } + return fallbackMethod; + } + +} diff --git a/resilience4j-feign/src/main/java/io/github/resilience4j/feign/FeignDecorator.java b/resilience4j-feign/src/main/java/io/github/resilience4j/feign/FeignDecorator.java new file mode 100644 index 0000000000..91d5de4d48 --- /dev/null +++ b/resilience4j-feign/src/main/java/io/github/resilience4j/feign/FeignDecorator.java @@ -0,0 +1,44 @@ +/* + * + * Copyright 2018 + * + * 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 io.github.resilience4j.feign; + +import java.lang.reflect.Method; + +import feign.InvocationHandlerFactory.MethodHandler; +import feign.Target; +import io.vavr.CheckedFunction1; + +/** + * Used to decorate methods defined by feign interfaces. Decorators can be stacked, allowing + * multiple Decorators to be combined. See {@link FeignDecorators}. + */ +@FunctionalInterface +public interface FeignDecorator { + + /** + * Decorates the invocation of a method defined by a feign interface. + * + * @param invocationCall represents the call to the method. This should be decorated by the + * implementing class. + * @param method the method of the feign interface that is invoked. + * @param methodHandler the feign methodHandler that executes the http request. + * @return the decorated invocationCall + */ + CheckedFunction1 decorate(CheckedFunction1 invocationCall, Method method, MethodHandler methodHandler, + Target target); + +} diff --git a/resilience4j-feign/src/main/java/io/github/resilience4j/feign/FeignDecorators.java b/resilience4j-feign/src/main/java/io/github/resilience4j/feign/FeignDecorators.java new file mode 100644 index 0000000000..f78698b797 --- /dev/null +++ b/resilience4j-feign/src/main/java/io/github/resilience4j/feign/FeignDecorators.java @@ -0,0 +1,156 @@ +/* + * + * Copyright 2018 + * + * 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 io.github.resilience4j.feign; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import feign.InvocationHandlerFactory.MethodHandler; +import feign.Target; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.vavr.CheckedFunction1; + +/** + * Builder to help build stacked decorators.
+ * + *
+ * {
+ *     @code
+ *     CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendName");
+ *     RateLimiter rateLimiter = RateLimiter.ofDefaults("backendName");
+ *     FeignDecorators decorators = FeignDecorators.builder()
+ *             .withCircuitBreaker(circuitBreaker)
+ *             .withRateLimiter(rateLimiter)
+ *             .build();
+ *     MyService myService = Resilience4jFeign.builder(decorators).target(MyService.class, "http://localhost:8080/");
+ * }
+ * 
+ * + * The order in which decorators are applied correspond to the order in which they are declared. For + * example, calling {@link FeignDecorators.Builder#withFallback(Object)} before + * {@link FeignDecorators.Builder#withCircuitBreaker(CircuitBreaker)} would mean that the fallback + * is called when the HTTP request fails, but would no longer be reachable if the CircuitBreaker + * were open. However, reversing the order would mean that the fallback is called both when the HTTP + * request fails and when the CircuitBreaker is open.
+ * So be wary of this when designing your "resilience" strategy. + */ +public class FeignDecorators implements FeignDecorator { + + private final List decorators; + + private FeignDecorators(List decorators) { + this.decorators = decorators; + } + + @Override + public CheckedFunction1 decorate(CheckedFunction1 fn, + Method method, MethodHandler methodHandler, Target target) { + CheckedFunction1 decoratedFn = fn; + for (final FeignDecorator decorator : decorators) { + decoratedFn = decorator.decorate(decoratedFn, method, methodHandler, target); + } + return decoratedFn; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private final List decorators = new ArrayList<>(); + + /** + * Adds a {@link CircuitBreaker} to the decorator chain. + * + * @param circuitBreaker a fully configured {@link CircuitBreaker}. + * @return the builder + */ + public Builder withCircuitBreaker(CircuitBreaker circuitBreaker) { + decorators.add((fn, m, mh, t) -> CircuitBreaker.decorateCheckedFunction(circuitBreaker, fn)); + return this; + } + + /** + * Adds a {@link RateLimiter} to the decorator chain. + * + * @param rateLimiter a fully configured {@link RateLimiter}. + * @return the builder + */ + public Builder withRateLimiter(RateLimiter rateLimiter) { + decorators.add((fn, m, mh, t) -> RateLimiter.decorateCheckedFunction(rateLimiter, fn)); + return this; + } + + /** + * Adds a fallback to the decorator chain. Multiple fallbacks can be applied with the next + * fallback being called when the previous one fails. + * + * @param fallback must match the feign interface, i.e. the interface specified when calling + * {@link Resilience4jFeign.Builder#target(Class, String)}. + * @return the builder + */ + public Builder withFallback(Object fallback) { + decorators.add(new FallbackDecorator(fallback)); + return this; + } + + /** + * Adds a fallback to the decorator chain. Multiple fallbacks can be applied with the next + * fallback being called when the previous one fails. + * + * @param fallback must match the feign interface, i.e. the interface specified when calling + * {@link Resilience4jFeign.Builder#target(Class, String)}. + * @param filter only {@link Exception}s matching the specified {@link Exception} will + * trigger the fallback. + * @return the builder + */ + public Builder withFallback(Object fallback, Class filter) { + decorators.add(new FallbackDecorator(fallback, filter)); + return this; + } + + /** + * Adds a fallback to the decorator chain. Multiple fallbacks can be applied with the next + * fallback being called when the previous one fails. + * + * @param fallback must match the feign interface, i.e. the interface specified when calling + * {@link Resilience4jFeign.Builder#target(Class, String)}. + * @param filter the filter must return true for the fallback to be called. + * @return the builder + */ + public Builder withFallback(Object fallback, Predicate filter) { + decorators.add(new FallbackDecorator(fallback, filter)); + return this; + } + + /** + * Builds the decorator chain. This can then be used to setup an instance of + * {@link Resilience4jFeign}. + * + * @return the decorators. + */ + public FeignDecorators build() { + return new FeignDecorators(decorators); + } + + } + +} diff --git a/resilience4j-feign/src/main/java/io/github/resilience4j/feign/Resilience4jFeign.java b/resilience4j-feign/src/main/java/io/github/resilience4j/feign/Resilience4jFeign.java new file mode 100644 index 0000000000..853cdc4977 --- /dev/null +++ b/resilience4j-feign/src/main/java/io/github/resilience4j/feign/Resilience4jFeign.java @@ -0,0 +1,66 @@ +/* + * + * Copyright 2018 + * + * 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 io.github.resilience4j.feign; + +import feign.Feign; +import feign.InvocationHandlerFactory; + +/** + * Main class for combining feign with Resilience4j. + * + *
+ * {@code
+ *     MyService myService = Resilience4jFeign.builder(decorators).target(MyService.class, "http://localhost:8080/");
+ * }
+ * 
+ * + * {@link Resilience4jFeign} works in the same way as the standard {@link Feign.Builder}. Only + * {@link Feign.Builder#invocationHandlerFactory(InvocationHandlerFactory)} may not be called as + * this is how {@link Resilience4jFeign} decorates the feign interface.
+ * See {@link FeignDecorators} on how to build decorators and enhance your feign interfaces. + */ +public final class Resilience4jFeign { + + public static Builder builder(FeignDecorator invocationDecorator) { + return new Builder(invocationDecorator); + } + + public static final class Builder extends Feign.Builder { + + private final FeignDecorator invocationDecorator; + + public Builder(FeignDecorator invocationDecorator) { + this.invocationDecorator = invocationDecorator; + } + + /** + * Will throw an {@link UnsupportedOperationException} exception. + */ + @Override + public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) { + throw new UnsupportedOperationException(); + } + + @Override + public Feign build() { + super.invocationHandlerFactory( + (target, dispatch) -> new DecoratorInvocationHandler(target, dispatch, invocationDecorator)); + return super.build(); + } + + } +} diff --git a/resilience4j-feign/src/test/java/io/github/resilience4j/feign/DecoratorInvocationHandlerTest.java b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/DecoratorInvocationHandlerTest.java new file mode 100644 index 0000000000..88eacf1c6b --- /dev/null +++ b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/DecoratorInvocationHandlerTest.java @@ -0,0 +1,140 @@ +/* + * + * Copyright 2018 + * + * 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 io.github.resilience4j.feign; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import feign.InvocationHandlerFactory.MethodHandler; +import feign.Target; +import feign.Target.HardCodedTarget; +import io.github.resilience4j.feign.test.TestFeignDecorator; +import io.github.resilience4j.feign.test.TestService; +import io.github.resilience4j.feign.test.TestServiceImpl; + +public class DecoratorInvocationHandlerTest { + + private DecoratorInvocationHandler testSubject; + private TestService testService; + private Method greetingMethod; + private TestFeignDecorator feignDecorator; + private MethodHandler methodHandler; + private Map dispatch; + private Target target; + + @Before + public void setUp() throws Throwable { + target = new HardCodedTarget(TestService.class, TestService.class.getSimpleName()); + testService = new TestServiceImpl(); + greetingMethod = testService.getClass().getDeclaredMethod("greeting"); + feignDecorator = new TestFeignDecorator(); + + methodHandler = mock(MethodHandler.class); + when(methodHandler.invoke(any())).thenReturn(testService.greeting()); + + dispatch = new HashMap<>(); + dispatch.put(greetingMethod, methodHandler); + + testSubject = new DecoratorInvocationHandler(target, dispatch, feignDecorator); + } + + @Test + public void testInvoke() throws Throwable { + final Object result = testSubject.invoke(testService, greetingMethod, new Object[0]); + + verify(methodHandler, times(1)).invoke(any()); + assertThat(feignDecorator.isCalled()) + .describedAs("FeignDecorator is called") + .isTrue(); + assertThat(result) + .describedAs("Return of invocation") + .isEqualTo(testService.greeting()); + } + + @Test + public void testDecorator() throws Throwable { + feignDecorator.setAlternativeFunction(fnArgs -> "AlternativeFunction"); + testSubject = new DecoratorInvocationHandler(target, dispatch, feignDecorator); + + final Object result = testSubject.invoke(testService, greetingMethod, new Object[0]); + + verify(methodHandler, times(0)).invoke(any()); + assertThat(feignDecorator.isCalled()) + .describedAs("FeignDecorator is called") + .isTrue(); + assertThat(result) + .describedAs("Return of invocation") + .isEqualTo("AlternativeFunction"); + } + + @Test + public void testInvokeToString() throws Throwable { + final Method toStringMethod = testService.getClass().getMethod("toString"); + + final Object result = testSubject.invoke(testService, toStringMethod, new Object[0]); + + verify(methodHandler, times(0)).invoke(any()); + assertThat(feignDecorator.isCalled()) + .describedAs("FeignDecorator is called") + .isTrue(); + assertThat(result) + .describedAs("Return of invocation") + .isEqualTo(target.toString()); + } + + @Test + public void testInvokeEquals() throws Throwable { + final Method equalsMethod = testService.getClass().getMethod("equals", Object.class); + + final Boolean result = (Boolean) testSubject.invoke(testService, equalsMethod, new Object[] {testSubject}); + + verify(methodHandler, times(0)).invoke(any()); + assertThat(feignDecorator.isCalled()) + .describedAs("FeignDecorator is called") + .isTrue(); + assertThat(result) + .describedAs("Return of invocation") + .isTrue(); + } + + + @Test + public void testInvokeHashcode() throws Throwable { + final Method hashCodeMethod = testService.getClass().getMethod("hashCode"); + + final Integer result = (Integer) testSubject.invoke(testService, hashCodeMethod, new Object[0]); + + verify(methodHandler, times(0)).invoke(any()); + assertThat(feignDecorator.isCalled()) + .describedAs("FeignDecorator is called") + .isTrue(); + assertThat(result) + .describedAs("Return of invocation") + .isEqualTo(target.hashCode()); + } +} diff --git a/resilience4j-feign/src/test/java/io/github/resilience4j/feign/FeignDecoratorsTest.java b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/FeignDecoratorsTest.java new file mode 100644 index 0000000000..1b6b54f500 --- /dev/null +++ b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/FeignDecoratorsTest.java @@ -0,0 +1,74 @@ +/* + * + * Copyright 2018 + * + * 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 io.github.resilience4j.feign; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.Test; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.ratelimiter.RateLimiter; + +public class FeignDecoratorsTest { + + @Test + public void testWithNothing() throws Throwable { + final FeignDecorators testSubject = FeignDecorators.builder().build(); + + final Object result = testSubject.decorate(args -> args[0], null, null, null).apply(new Object[] {"test01"}); + + assertThat(result) + .describedAs("Returned result is correct") + .isEqualTo("test01"); + } + + + @Test + public void testWithCircuitBreaker() throws Throwable { + final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("test"); + final CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics(); + final FeignDecorators testSubject = FeignDecorators.builder().withCircuitBreaker(circuitBreaker).build(); + + + final Object result = testSubject.decorate(args -> args[0], null, null, null).apply(new Object[] {"test01"}); + + assertThat(result) + .describedAs("Returned result is correct") + .isEqualTo("test01"); + assertThat(metrics.getNumberOfSuccessfulCalls()) + .describedAs("Successful Calls") + .isEqualTo(1); + } + + + @Test + public void testWithRateLimiter() throws Throwable { + final RateLimiter rateLimiter = spy(RateLimiter.ofDefaults("test")); + final FeignDecorators testSubject = FeignDecorators.builder().withRateLimiter(rateLimiter).build(); + + final Object result = testSubject.decorate(args -> args[0], null, null, null).apply(new Object[] {"test01"}); + + assertThat(result) + .describedAs("Returned result is correct") + .isEqualTo("test01"); + verify(rateLimiter, times(1)).getPermission(any()); + } +} diff --git a/resilience4j-feign/src/test/java/io/github/resilience4j/feign/Resilience4jFeignCircuitBreakerTest.java b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/Resilience4jFeignCircuitBreakerTest.java new file mode 100644 index 0000000000..9e2929ed01 --- /dev/null +++ b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/Resilience4jFeignCircuitBreakerTest.java @@ -0,0 +1,161 @@ +/* + * + * Copyright 2018 + * + * 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 io.github.resilience4j.feign; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; + +import feign.FeignException; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import io.github.resilience4j.circuitbreaker.CircuitBreakerOpenException; +import io.github.resilience4j.feign.test.TestService; + +/** + * Tests the integration of the {@link Resilience4jFeign} with {@link CircuitBreaker} + */ +public class Resilience4jFeignCircuitBreakerTest { + + @Rule + public WireMockRule wireMockRule = new WireMockRule(); + + private static final CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() + .ringBufferSizeInClosedState(3) + .waitDurationInOpenState(Duration.ofMillis(1000)) + .build(); + + private CircuitBreaker circuitBreaker; + private TestService testService; + + @Before + public void setUp() { + circuitBreaker = CircuitBreaker.of("test", circuitBreakerConfig); + final FeignDecorators decorators = FeignDecorators.builder().withCircuitBreaker(circuitBreaker).build(); + testService = Resilience4jFeign.builder(decorators).target(TestService.class, "http://localhost:8080/"); + } + + @Test + public void testSuccessfulCall() throws Exception { + final CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics(); + + setupStub(200); + + testService.greeting(); + + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + assertThat(metrics.getNumberOfSuccessfulCalls()) + .describedAs("Successful Calls") + .isEqualTo(1); + } + + @Test + public void testFailedCall() throws Exception { + final CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics(); + boolean exceptionThrown = false; + + setupStub(400); + + try { + testService.greeting(); + } catch (final FeignException ex) { + exceptionThrown = true; + } + + assertThat(exceptionThrown) + .describedAs("FeignException thrown") + .isTrue(); + assertThat(metrics.getNumberOfFailedCalls()) + .describedAs("Successful Calls") + .isEqualTo(1); + } + + @Test + public void testCircuitBreakerOpen() throws Exception { + boolean exceptionThrown = false; + final int threshold = circuitBreaker + .getCircuitBreakerConfig() + .getRingBufferSizeInClosedState() + 1; + + setupStub(400); + + for (int i = 0; i < threshold; i++) { + try { + testService.greeting(); + } catch (final FeignException ex) { + // ignore + } catch (final CircuitBreakerOpenException ex) { + exceptionThrown = true; + } + } + + assertThat(exceptionThrown) + .describedAs("CircuitBreakerOpenException thrown") + .isTrue(); + assertThat(circuitBreaker.isCallPermitted()) + .describedAs("CircuitBreaker Closed") + .isFalse(); + } + + + @Test + public void testCircuitBreakerClosed() throws Exception { + boolean exceptionThrown = false; + final int threshold = circuitBreaker + .getCircuitBreakerConfig() + .getRingBufferSizeInClosedState() - 1; + + setupStub(400); + + for (int i = 0; i < threshold; i++) { + try { + testService.greeting(); + } catch (final FeignException ex) { + // ignore + } catch (final CircuitBreakerOpenException ex) { + exceptionThrown = true; + } + } + + assertThat(exceptionThrown) + .describedAs("CircuitBreakerOpenException thrown") + .isFalse(); + assertThat(circuitBreaker.isCallPermitted()) + .describedAs("CircuitBreaker Closed") + .isTrue(); + } + + private void setupStub(int responseCode) { + stubFor(get(urlPathEqualTo("/greeting")) + .willReturn(aResponse() + .withStatus(responseCode) + .withHeader("Content-Type", "text/plain") + .withBody("hello world"))); + } +} diff --git a/resilience4j-feign/src/test/java/io/github/resilience4j/feign/Resilience4jFeignFallbackTest.java b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/Resilience4jFeignFallbackTest.java new file mode 100644 index 0000000000..156f7516f2 --- /dev/null +++ b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/Resilience4jFeignFallbackTest.java @@ -0,0 +1,205 @@ +/* + * + * Copyright 2018 + * + * 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 io.github.resilience4j.feign; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; + +import feign.FeignException; +import io.github.resilience4j.circuitbreaker.CircuitBreakerOpenException; +import io.github.resilience4j.feign.test.TestService; + +/** + * Tests the integration of the {@link Resilience4jFeign} with a fallback. + */ +public class Resilience4jFeignFallbackTest { + + private static final String MOCK_URL = "http://localhost:8080/"; + + @Rule + public WireMockRule wireMockRule = new WireMockRule(); + + private TestService testService; + private TestService testServiceFallback; + + @Before + public void setUp() { + testServiceFallback = mock(TestService.class); + when(testServiceFallback.greeting()).thenReturn("fallback"); + + final FeignDecorators decorators = FeignDecorators.builder() + .withFallback(testServiceFallback) + .build(); + + testService = Resilience4jFeign.builder(decorators).target(TestService.class, MOCK_URL); + } + + @Test + public void testSuccessful() throws Exception { + setupStub(200); + + final String result = testService.greeting(); + + assertThat(result).describedAs("Result").isEqualTo("Hello, world!"); + verify(testServiceFallback, times(0)).greeting(); + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidFallback() throws Throwable { + final FeignDecorators decorators = FeignDecorators.builder().withFallback("not a fallback").build(); + Resilience4jFeign.builder(decorators).target(TestService.class, MOCK_URL); + } + + @Test + public void testFallback() throws Exception { + setupStub(400); + + final String result = testService.greeting(); + + assertThat(result).describedAs("Result").isNotEqualTo("Hello, world!"); + assertThat(result).describedAs("Result").isEqualTo("fallback"); + verify(testServiceFallback, times(1)).greeting(); + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + } + + + @Test + public void testFallbackExceptionFilter() throws Exception { + final TestService testServiceExceptionFallback = mock(TestService.class); + when(testServiceExceptionFallback.greeting()).thenReturn("exception fallback"); + + final FeignDecorators decorators = FeignDecorators.builder() + .withFallback(testServiceExceptionFallback, FeignException.class) + .withFallback(testServiceFallback) + .build(); + + testService = Resilience4jFeign.builder(decorators).target(TestService.class, MOCK_URL); + setupStub(400); + + final String result = testService.greeting(); + + assertThat(result).describedAs("Result").isNotEqualTo("Hello, world!"); + assertThat(result).describedAs("Result").isEqualTo("exception fallback"); + verify(testServiceFallback, times(0)).greeting(); + verify(testServiceExceptionFallback, times(1)).greeting(); + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + } + + @Test + public void testFallbackExceptionFilterNotCalled() throws Exception { + final TestService testServiceExceptionFallback = mock(TestService.class); + when(testServiceExceptionFallback.greeting()).thenReturn("exception fallback"); + + final FeignDecorators decorators = FeignDecorators.builder() + .withFallback(testServiceExceptionFallback, CircuitBreakerOpenException.class) + .withFallback(testServiceFallback) + .build(); + + testService = Resilience4jFeign.builder(decorators).target(TestService.class, MOCK_URL); + setupStub(400); + + final String result = testService.greeting(); + + assertThat(result).describedAs("Result").isNotEqualTo("Hello, world!"); + assertThat(result).describedAs("Result").isEqualTo("fallback"); + verify(testServiceFallback, times(1)).greeting(); + verify(testServiceExceptionFallback, times(0)).greeting(); + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + } + + @Test + public void testFallbackFilter() throws Exception { + final TestService testServiceFilterFallback = mock(TestService.class); + when(testServiceFilterFallback.greeting()).thenReturn("filter fallback"); + + final FeignDecorators decorators = FeignDecorators.builder() + .withFallback(testServiceFilterFallback, ex -> true) + .withFallback(testServiceFallback) + .build(); + + testService = Resilience4jFeign.builder(decorators).target(TestService.class, MOCK_URL); + setupStub(400); + + final String result = testService.greeting(); + + assertThat(result).describedAs("Result").isNotEqualTo("Hello, world!"); + assertThat(result).describedAs("Result").isEqualTo("filter fallback"); + verify(testServiceFallback, times(0)).greeting(); + verify(testServiceFilterFallback, times(1)).greeting(); + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + } + + @Test + public void testFallbackFilterNotCalled() throws Exception { + final TestService testServiceFilterFallback = mock(TestService.class); + when(testServiceFilterFallback.greeting()).thenReturn("filter fallback"); + + final FeignDecorators decorators = FeignDecorators.builder() + .withFallback(testServiceFilterFallback, ex -> false) + .withFallback(testServiceFallback) + .build(); + + testService = Resilience4jFeign.builder(decorators).target(TestService.class, MOCK_URL); + setupStub(400); + + final String result = testService.greeting(); + + assertThat(result).describedAs("Result").isNotEqualTo("Hello, world!"); + assertThat(result).describedAs("Result").isEqualTo("fallback"); + verify(testServiceFallback, times(1)).greeting(); + verify(testServiceFilterFallback, times(0)).greeting(); + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + } + + @Test + public void testRevertFallback() throws Exception { + setupStub(400); + + testService.greeting(); + setupStub(200); + final String result = testService.greeting(); + + assertThat(result).describedAs("Result").isEqualTo("Hello, world!"); + verify(testServiceFallback, times(1)).greeting(); + verify(2, getRequestedFor(urlPathEqualTo("/greeting"))); + + } + + private void setupStub(int responseCode) { + stubFor(get(urlPathEqualTo("/greeting")) + .willReturn(aResponse() + .withStatus(responseCode) + .withHeader("Content-Type", "text/plain") + .withBody("Hello, world!"))); + } +} diff --git a/resilience4j-feign/src/test/java/io/github/resilience4j/feign/Resilience4jRateLimiterTest.java b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/Resilience4jRateLimiterTest.java new file mode 100644 index 0000000000..21674f3cca --- /dev/null +++ b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/Resilience4jRateLimiterTest.java @@ -0,0 +1,110 @@ +/* + * + * Copyright 2018 + * + * 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 io.github.resilience4j.feign; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.time.Duration.ofMillis; +import static java.time.Duration.ofSeconds; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; + +import feign.FeignException; +import io.github.resilience4j.feign.test.TestService; +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.RateLimiterConfig; +import io.github.resilience4j.ratelimiter.RequestNotPermitted; + +/** + * Tests the integration of the {@link Resilience4jFeign} with {@link RateLimiter} + */ +public class Resilience4jRateLimiterTest { + + @Rule + public WireMockRule wireMockRule = new WireMockRule(); + + private static final RateLimiterConfig config = RateLimiterConfig.custom() + .timeoutDuration(ofMillis(50)) + .limitRefreshPeriod(ofSeconds(5)) + .limitForPeriod(1) + .build(); + + private TestService testService; + + @Before + public void setUp() { + final RateLimiter rateLimiter = RateLimiter.of("backendName", config); + final FeignDecorators decorators = FeignDecorators.builder().withRateLimiter(rateLimiter).build(); + testService = Resilience4jFeign.builder(decorators).target(TestService.class, "http://localhost:8080/"); + } + + @Test + public void testSuccessfulCall() throws Exception { + setupStub(200); + + testService.greeting(); + + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + } + + @Test(expected = RequestNotPermitted.class) + public void testRatelimterLimiting() throws Exception { + setupStub(200); + + testService.greeting(); + testService.greeting(); + + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + } + + @Test(expected = RequestNotPermitted.class) + public void testRatelimterNotLimiting() throws Exception { + setupStub(200); + + testService.greeting(); + Thread.sleep(1_000); + testService.greeting(); + Thread.sleep(1_000); + testService.greeting(); + + verify(1, getRequestedFor(urlPathEqualTo("/greeting"))); + } + + @Test(expected = FeignException.class) + public void testFailedHttpCall() throws Exception { + setupStub(400); + + testService.greeting(); + } + + + private void setupStub(int responseCode) { + stubFor(get(urlPathEqualTo("/greeting")) + .willReturn(aResponse() + .withStatus(responseCode) + .withHeader("Content-Type", "text/plain") + .withBody("hello world"))); + } +} diff --git a/resilience4j-feign/src/test/java/io/github/resilience4j/feign/test/TestFeignDecorator.java b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/test/TestFeignDecorator.java new file mode 100644 index 0000000000..44d5136996 --- /dev/null +++ b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/test/TestFeignDecorator.java @@ -0,0 +1,39 @@ +package io.github.resilience4j.feign.test; + +import java.lang.reflect.Method; + +import feign.InvocationHandlerFactory.MethodHandler; +import feign.Target; +import io.github.resilience4j.feign.FeignDecorator; +import io.vavr.CheckedFunction1; + +public class TestFeignDecorator implements FeignDecorator { + + private volatile boolean called = false; + private volatile CheckedFunction1 alternativeFunction; + + public boolean isCalled() { + return called; + } + + public void setCalled(boolean called) { + this.called = called; + } + + public CheckedFunction1 getAlternativeFunction() { + return alternativeFunction; + } + + public void setAlternativeFunction(CheckedFunction1 alternativeFunction) { + this.alternativeFunction = alternativeFunction; + } + + @Override + public CheckedFunction1 decorate(CheckedFunction1 invocationCall, + Method method, MethodHandler methodHandler, + Target target) { + called = true; + return alternativeFunction != null ? alternativeFunction : invocationCall; + } + +} diff --git a/resilience4j-feign/src/test/java/io/github/resilience4j/feign/test/TestService.java b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/test/TestService.java new file mode 100644 index 0000000000..3ec23fcdb4 --- /dev/null +++ b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/test/TestService.java @@ -0,0 +1,12 @@ +package io.github.resilience4j.feign.test; + +import feign.RequestLine; + + +public interface TestService { + + @RequestLine("GET /greeting") + String greeting(); + + +} diff --git a/resilience4j-feign/src/test/java/io/github/resilience4j/feign/test/TestServiceImpl.java b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/test/TestServiceImpl.java new file mode 100644 index 0000000000..5f93b07098 --- /dev/null +++ b/resilience4j-feign/src/test/java/io/github/resilience4j/feign/test/TestServiceImpl.java @@ -0,0 +1,10 @@ +package io.github.resilience4j.feign.test; + +public class TestServiceImpl implements TestService { + + @Override + public String greeting() { + return "testGreeting"; + } + +} diff --git a/settings.gradle b/settings.gradle index 22c3331530..57606a004d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,6 +17,7 @@ include 'resilience4j-spring' include 'resilience4j-spring-boot' include 'resilience4j-spring-boot2' include 'resilience4j-retrofit' +include 'resilience4j-feign' include 'resilience4j-ratpack' include 'resilience4j-prometheus' include 'resilience4j-timelimiter'