Marcin Grzejszczak
4 years ago
13 changed files with 892 additions and 6 deletions
@ -0,0 +1,87 @@
@@ -0,0 +1,87 @@
|
||||
/* |
||||
* Copyright 2013-2020 the original author or authors. |
||||
* |
||||
* 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 |
||||
* |
||||
* https://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 org.springframework.cloud.openfeign; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
|
||||
import static feign.Util.checkNotNull; |
||||
|
||||
/** |
||||
* Used to control the fallback given its cause. |
||||
* |
||||
* Ex. |
||||
* |
||||
* <pre> |
||||
* {@code |
||||
* // This instance will be invoked if there are errors of any kind.
|
||||
* FallbackFactory<GitHub> fallbackFactory = cause -> (owner, repo) -> { |
||||
* if (cause instanceof FeignException && ((FeignException) cause).status() == 403) { |
||||
* return Collections.emptyList(); |
||||
* } else { |
||||
* return Arrays.asList("yogi"); |
||||
* } |
||||
* }; |
||||
* |
||||
* GitHub github = FeignCircuitBreaker.builder() |
||||
* ... |
||||
* .target(GitHub.class, "https://api.github.com", fallbackFactory); |
||||
* } |
||||
* </pre> |
||||
* |
||||
* @param <T> the feign interface type |
||||
*/ |
||||
public interface FallbackFactory<T> { |
||||
|
||||
/** |
||||
* Returns an instance of the fallback appropriate for the given cause. |
||||
* @param cause cause of an exception. |
||||
* @return fallback |
||||
*/ |
||||
T create(Throwable cause); |
||||
|
||||
final class Default<T> implements FallbackFactory<T> { |
||||
|
||||
final Log logger; |
||||
|
||||
final T constant; |
||||
|
||||
public Default(T constant) { |
||||
this(constant, LogFactory.getLog(Default.class)); |
||||
} |
||||
|
||||
Default(T constant, Log logger) { |
||||
this.constant = checkNotNull(constant, "fallback"); |
||||
this.logger = checkNotNull(logger, "logger"); |
||||
} |
||||
|
||||
@Override |
||||
public T create(Throwable cause) { |
||||
if (logger.isTraceEnabled()) { |
||||
logger.trace("fallback due to: " + cause.getMessage(), cause); |
||||
} |
||||
return constant; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return constant.toString(); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
/* |
||||
* Copyright 2013-2020 the original author or authors. |
||||
* |
||||
* 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 |
||||
* |
||||
* https://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 org.springframework.cloud.openfeign; |
||||
|
||||
import java.lang.reflect.InvocationHandler; |
||||
import java.lang.reflect.Method; |
||||
import java.util.Map; |
||||
|
||||
import feign.Feign; |
||||
import feign.InvocationHandlerFactory; |
||||
import feign.Target; |
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; |
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; |
||||
|
||||
/** |
||||
* Allows Feign interfaces to work with {@link CircuitBreaker}. |
||||
* |
||||
* @author Marcin Grzejszczak |
||||
* @since 3.0.0 |
||||
*/ |
||||
public final class FeignCircuitBreaker { |
||||
|
||||
private FeignCircuitBreaker() { |
||||
throw new IllegalStateException("Don't instantiate a utility class"); |
||||
} |
||||
|
||||
/** |
||||
* @return builder for Feign CircuitBreaker integration |
||||
*/ |
||||
public static Builder builder() { |
||||
return new Builder(); |
||||
} |
||||
|
||||
/** |
||||
* Builder for Feign CircuitBreaker integration. |
||||
*/ |
||||
public static final class Builder extends Feign.Builder { |
||||
|
||||
private CircuitBreakerFactory circuitBreakerFactory; |
||||
|
||||
private String feignClientName; |
||||
|
||||
Builder circuitBreakerFactory(CircuitBreakerFactory circuitBreakerFactory) { |
||||
this.circuitBreakerFactory = circuitBreakerFactory; |
||||
return this; |
||||
} |
||||
|
||||
Builder feignClientName(String feignClientName) { |
||||
this.feignClientName = feignClientName; |
||||
return this; |
||||
} |
||||
|
||||
public <T> T target(Target<T> target, T fallback) { |
||||
return build( |
||||
fallback != null ? new FallbackFactory.Default<T>(fallback) : null) |
||||
.newInstance(target); |
||||
} |
||||
|
||||
public <T> T target(Target<T> target, |
||||
FallbackFactory<? extends T> fallbackFactory) { |
||||
return build(fallbackFactory).newInstance(target); |
||||
} |
||||
|
||||
public Feign build(final FallbackFactory<?> nullableFallbackFactory) { |
||||
super.invocationHandlerFactory(new InvocationHandlerFactory() { |
||||
@Override |
||||
public InvocationHandler create(Target target, |
||||
Map<Method, MethodHandler> dispatch) { |
||||
return new FeignCircuitBreakerInvocationHandler(circuitBreakerFactory, |
||||
feignClientName, target, dispatch, nullableFallbackFactory); |
||||
} |
||||
}); |
||||
return super.build(); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
/* |
||||
* Copyright 2013-2020 the original author or authors. |
||||
* |
||||
* 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 |
||||
* |
||||
* https://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 org.springframework.cloud.openfeign; |
||||
|
||||
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
||||
class FeignCircuitBreakerDisabledConditions extends AnyNestedCondition { |
||||
|
||||
FeignCircuitBreakerDisabledConditions() { |
||||
super(ConfigurationPhase.PARSE_CONFIGURATION); |
||||
} |
||||
|
||||
@ConditionalOnMissingClass("org.springframework.cloud.client.circuitbreaker.CircuitBreaker") |
||||
static class CircuitBreakerClassMissing { |
||||
|
||||
} |
||||
|
||||
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "false", |
||||
matchIfMissing = true) |
||||
static class CircuitBreakerDisabled { |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,152 @@
@@ -0,0 +1,152 @@
|
||||
/* |
||||
* Copyright 2013-2020 the original author or authors. |
||||
* |
||||
* 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 |
||||
* |
||||
* https://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 org.springframework.cloud.openfeign; |
||||
|
||||
import java.lang.reflect.InvocationHandler; |
||||
import java.lang.reflect.Method; |
||||
import java.lang.reflect.Proxy; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
import java.util.function.Function; |
||||
import java.util.function.Supplier; |
||||
|
||||
import feign.InvocationHandlerFactory; |
||||
import feign.Target; |
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; |
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; |
||||
|
||||
import static feign.Util.checkNotNull; |
||||
|
||||
class FeignCircuitBreakerInvocationHandler implements InvocationHandler { |
||||
|
||||
private final CircuitBreakerFactory factory; |
||||
|
||||
private final String feignClientName; |
||||
|
||||
private final Target<?> target; |
||||
|
||||
private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch; |
||||
|
||||
private final FallbackFactory<?> nullableFallbackFactory; |
||||
|
||||
private final Map<Method, Method> fallbackMethodMap; |
||||
|
||||
FeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, |
||||
String feignClientName, Target<?> target, |
||||
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, |
||||
FallbackFactory<?> nullableFallbackFactory) { |
||||
this.factory = factory; |
||||
this.feignClientName = feignClientName; |
||||
this.target = checkNotNull(target, "target"); |
||||
this.dispatch = checkNotNull(dispatch, "dispatch"); |
||||
this.fallbackMethodMap = toFallbackMethod(dispatch); |
||||
this.nullableFallbackFactory = nullableFallbackFactory; |
||||
} |
||||
|
||||
@Override |
||||
public Object invoke(final Object proxy, final Method method, final Object[] args) |
||||
throws Throwable { |
||||
// early exit if the invoked method is from java.lang.Object
|
||||
// code is the same as ReflectiveFeign.FeignInvocationHandler
|
||||
if ("equals".equals(method.getName())) { |
||||
try { |
||||
Object otherHandler = args.length > 0 && args[0] != null |
||||
? Proxy.getInvocationHandler(args[0]) : null; |
||||
return equals(otherHandler); |
||||
} |
||||
catch (IllegalArgumentException e) { |
||||
return false; |
||||
} |
||||
} |
||||
else if ("hashCode".equals(method.getName())) { |
||||
return hashCode(); |
||||
} |
||||
else if ("toString".equals(method.getName())) { |
||||
return toString(); |
||||
} |
||||
String circuitName = this.feignClientName + "_" + method.getName(); |
||||
CircuitBreaker circuitBreaker = this.factory.create(circuitName); |
||||
Supplier<Object> supplier = asSupplier(method, args); |
||||
if (this.nullableFallbackFactory != null) { |
||||
Function<Throwable, Object> fallbackFunction = throwable -> { |
||||
Object fallback = this.nullableFallbackFactory.create(throwable); |
||||
try { |
||||
return this.fallbackMethodMap.get(method).invoke(fallback, args); |
||||
} |
||||
catch (Exception e) { |
||||
throw new IllegalStateException(e); |
||||
} |
||||
}; |
||||
return circuitBreaker.run(supplier, fallbackFunction); |
||||
} |
||||
return circuitBreaker.run(supplier); |
||||
} |
||||
|
||||
private Supplier<Object> asSupplier(final Method method, final Object[] args) { |
||||
return () -> { |
||||
try { |
||||
return this.dispatch.get(method).invoke(args); |
||||
} |
||||
catch (RuntimeException throwable) { |
||||
throw throwable; |
||||
} |
||||
catch (Throwable throwable) { |
||||
throw new RuntimeException(throwable); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* If the method param of InvocationHandler.invoke is not accessible, i.e in a |
||||
* package-private interface, the fallback call will 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 |
||||
*/ |
||||
static Map<Method, Method> toFallbackMethod( |
||||
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) { |
||||
Map<Method, Method> result = new LinkedHashMap<Method, Method>(); |
||||
for (Method method : dispatch.keySet()) { |
||||
method.setAccessible(true); |
||||
result.put(method, method); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (obj instanceof FeignCircuitBreakerInvocationHandler) { |
||||
FeignCircuitBreakerInvocationHandler other = (FeignCircuitBreakerInvocationHandler) obj; |
||||
return this.target.equals(other.target); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return this.target.hashCode(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this.target.toString(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
/* |
||||
* Copyright 2013-2020 the original author or authors. |
||||
* |
||||
* 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 |
||||
* |
||||
* https://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 org.springframework.cloud.openfeign; |
||||
|
||||
import feign.Feign; |
||||
import feign.Target; |
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
class FeignCircuitBreakerTargeter implements Targeter { |
||||
|
||||
private final CircuitBreakerFactory circuitBreakerFactory; |
||||
|
||||
FeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory) { |
||||
this.circuitBreakerFactory = circuitBreakerFactory; |
||||
} |
||||
|
||||
@Override |
||||
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, |
||||
FeignContext context, Target.HardCodedTarget<T> target) { |
||||
if (!(feign instanceof FeignCircuitBreaker.Builder)) { |
||||
return feign.target(target); |
||||
} |
||||
FeignCircuitBreaker.Builder builder = (FeignCircuitBreaker.Builder) feign; |
||||
String name = !StringUtils.hasText(factory.getContextId()) ? factory.getName() |
||||
: factory.getContextId(); |
||||
Class<?> fallback = factory.getFallback(); |
||||
if (fallback != void.class) { |
||||
return targetWithFallback(name, context, target, builder, fallback); |
||||
} |
||||
Class<?> fallbackFactory = factory.getFallbackFactory(); |
||||
if (fallbackFactory != void.class) { |
||||
return targetWithFallbackFactory(name, context, target, builder, |
||||
fallbackFactory); |
||||
} |
||||
return builder(name, builder).target(target); |
||||
} |
||||
|
||||
private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context, |
||||
Target.HardCodedTarget<T> target, FeignCircuitBreaker.Builder builder, |
||||
Class<?> fallbackFactoryClass) { |
||||
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext( |
||||
"fallbackFactory", feignClientName, context, fallbackFactoryClass, |
||||
FallbackFactory.class); |
||||
return builder(feignClientName, builder).target(target, fallbackFactory); |
||||
} |
||||
|
||||
private <T> T targetWithFallback(String feignClientName, FeignContext context, |
||||
Target.HardCodedTarget<T> target, FeignCircuitBreaker.Builder builder, |
||||
Class<?> fallback) { |
||||
T fallbackInstance = getFromContext("fallback", feignClientName, context, |
||||
fallback, target.type()); |
||||
return builder(feignClientName, builder).target(target, fallbackInstance); |
||||
} |
||||
|
||||
private <T> T getFromContext(String fallbackMechanism, String feignClientName, |
||||
FeignContext context, Class<?> beanType, Class<T> targetType) { |
||||
Object fallbackInstance = context.getInstance(feignClientName, beanType); |
||||
if (fallbackInstance == null) { |
||||
throw new IllegalStateException(String.format( |
||||
"No " + fallbackMechanism |
||||
+ " instance of type %s found for feign client %s", |
||||
beanType, feignClientName)); |
||||
} |
||||
|
||||
if (!targetType.isAssignableFrom(beanType)) { |
||||
throw new IllegalStateException(String.format("Incompatible " |
||||
+ fallbackMechanism |
||||
+ " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", |
||||
beanType, targetType, feignClientName)); |
||||
} |
||||
return (T) fallbackInstance; |
||||
} |
||||
|
||||
private FeignCircuitBreaker.Builder builder(String feignClientName, |
||||
FeignCircuitBreaker.Builder builder) { |
||||
return builder.circuitBreakerFactory(this.circuitBreakerFactory) |
||||
.feignClientName(feignClientName); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,317 @@
@@ -0,0 +1,317 @@
|
||||
/* |
||||
* Copyright 2013-2020 the original author or authors. |
||||
* |
||||
* 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 |
||||
* |
||||
* https://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 org.springframework.cloud.openfeign.circuitbreaker; |
||||
|
||||
import java.util.Objects; |
||||
import java.util.concurrent.atomic.AtomicBoolean; |
||||
import java.util.function.Function; |
||||
import java.util.function.Supplier; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.junit.jupiter.api.AfterAll; |
||||
import org.junit.jupiter.api.BeforeAll; |
||||
import org.junit.runner.RunWith; |
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; |
||||
import org.springframework.boot.web.server.LocalServerPort; |
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; |
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; |
||||
import org.springframework.cloud.client.circuitbreaker.ConfigBuilder; |
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException; |
||||
import org.springframework.cloud.openfeign.EnableFeignClients; |
||||
import org.springframework.cloud.openfeign.FallbackFactory; |
||||
import org.springframework.cloud.openfeign.FeignClient; |
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.stereotype.Component; |
||||
import org.springframework.test.annotation.DirtiesContext; |
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; |
||||
import org.springframework.util.SocketUtils; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RequestMethod; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* @author Spencer Gibb |
||||
*/ |
||||
@RunWith(SpringJUnit4ClassRunner.class) |
||||
@SpringBootTest(classes = CirciutBreakerTests.Application.class, |
||||
webEnvironment = WebEnvironment.DEFINED_PORT, |
||||
value = { "spring.application.name=springcircuittest", "spring.jmx.enabled=false", |
||||
"feign.circuitbreaker.enabled=true" }) |
||||
@DirtiesContext |
||||
public class CirciutBreakerTests { |
||||
|
||||
@Autowired |
||||
MyCircuitBreaker myCircuitBreaker; |
||||
|
||||
@Autowired |
||||
TestClient testClient; |
||||
|
||||
@Autowired |
||||
TestClientWithFactory testClientWithFactory; |
||||
|
||||
@LocalServerPort |
||||
private int port = 0; |
||||
|
||||
@BeforeAll |
||||
public static void beforeClass() { |
||||
System.setProperty("server.port", |
||||
String.valueOf(SocketUtils.findAvailableTcpPort())); |
||||
} |
||||
|
||||
@AfterAll |
||||
public static void afterClass() { |
||||
System.clearProperty("server.port"); |
||||
} |
||||
|
||||
@Before |
||||
public void setup() { |
||||
this.myCircuitBreaker.clear(); |
||||
} |
||||
|
||||
@Test |
||||
public void testSimpleTypeWithFallback() { |
||||
Hello hello = testClient.getHello(); |
||||
|
||||
assertThat(hello).as("hello was null").isNotNull(); |
||||
assertThat(hello).as("first hello didn't match") |
||||
.isEqualTo(new Hello("hello world 1")); |
||||
assertThat(myCircuitBreaker.runWasCalled).as("Circuit Breaker was called") |
||||
.isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void test404WithFallback() { |
||||
assertThat(testClient.getException()).isEqualTo("Fixed response"); |
||||
} |
||||
|
||||
@Test |
||||
public void testSimpleTypeWithFallbackFactory() { |
||||
Hello hello = testClientWithFactory.getHello(); |
||||
|
||||
assertThat(hello).as("hello was null").isNotNull(); |
||||
assertThat(hello).as("first hello didn't match") |
||||
.isEqualTo(new Hello("hello world 1")); |
||||
assertThat(myCircuitBreaker.runWasCalled).as("Circuit Breaker was called") |
||||
.isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void test404WithFallbackFactory() { |
||||
assertThat(testClientWithFactory.getException()).isEqualTo("Fixed response"); |
||||
} |
||||
|
||||
// tag::client_with_fallback[]
|
||||
@FeignClient(name = "test", url = "http://localhost:${server.port}/", |
||||
fallback = Fallback.class) |
||||
protected interface TestClient { |
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello") |
||||
Hello getHello(); |
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound") |
||||
String getException(); |
||||
|
||||
} |
||||
|
||||
@Component |
||||
static class Fallback implements TestClient { |
||||
|
||||
@Override |
||||
public Hello getHello() { |
||||
throw new NoFallbackAvailableException("Boom!", new RuntimeException()); |
||||
} |
||||
|
||||
@Override |
||||
public String getException() { |
||||
return "Fixed response"; |
||||
} |
||||
|
||||
} |
||||
// end::client_with_fallback[]
|
||||
|
||||
// tag::client_with_fallback_factory[]
|
||||
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/", |
||||
fallbackFactory = TestFallbackFactory.class) |
||||
protected interface TestClientWithFactory { |
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello") |
||||
Hello getHello(); |
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound") |
||||
String getException(); |
||||
|
||||
} |
||||
|
||||
@Component |
||||
static class TestFallbackFactory implements FallbackFactory<FallbackWithFactory> { |
||||
|
||||
@Override |
||||
public FallbackWithFactory create(Throwable cause) { |
||||
return new FallbackWithFactory(); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class FallbackWithFactory implements TestClientWithFactory { |
||||
|
||||
@Override |
||||
public Hello getHello() { |
||||
throw new NoFallbackAvailableException("Boom!", new RuntimeException()); |
||||
} |
||||
|
||||
@Override |
||||
public String getException() { |
||||
return "Fixed response"; |
||||
} |
||||
|
||||
} |
||||
// end::client_with_fallback_factory[]
|
||||
|
||||
public static class Hello { |
||||
|
||||
private String message; |
||||
|
||||
public Hello() { |
||||
} |
||||
|
||||
public Hello(String message) { |
||||
this.message = message; |
||||
} |
||||
|
||||
public String getMessage() { |
||||
return this.message; |
||||
} |
||||
|
||||
public void setMessage(String message) { |
||||
this.message = message; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object o) { |
||||
if (this == o) { |
||||
return true; |
||||
} |
||||
if (o == null || getClass() != o.getClass()) { |
||||
return false; |
||||
} |
||||
Hello that = (Hello) o; |
||||
return Objects.equals(this.message, that.message); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return Objects.hash(this.message); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
@EnableAutoConfiguration |
||||
@RestController |
||||
@EnableFeignClients(clients = { TestClient.class, TestClientWithFactory.class }) |
||||
@Import(NoSecurityConfiguration.class) |
||||
protected static class Application implements TestClient { |
||||
|
||||
static final Log log = LogFactory.getLog(Application.class); |
||||
|
||||
@Bean |
||||
MyCircuitBreaker myCircuitBreaker() { |
||||
return new MyCircuitBreaker(); |
||||
} |
||||
|
||||
@Bean |
||||
CircuitBreakerFactory circuitBreakerFactory(MyCircuitBreaker myCircuitBreaker) { |
||||
return new CircuitBreakerFactory() { |
||||
@Override |
||||
public CircuitBreaker create(String id) { |
||||
log.info("Creating a circuit breaker with id [" + id + "]"); |
||||
return myCircuitBreaker; |
||||
} |
||||
|
||||
@Override |
||||
protected ConfigBuilder configBuilder(String id) { |
||||
return Object::new; |
||||
} |
||||
|
||||
@Override |
||||
public void configureDefault(Function defaultConfiguration) { |
||||
|
||||
} |
||||
}; |
||||
} |
||||
|
||||
@Override |
||||
public Hello getHello() { |
||||
return new Hello("hello world 1"); |
||||
} |
||||
|
||||
@Override |
||||
public String getException() { |
||||
throw new IllegalStateException("BOOM!"); |
||||
} |
||||
|
||||
@Bean |
||||
Fallback fallback() { |
||||
return new Fallback(); |
||||
} |
||||
|
||||
@Bean |
||||
TestFallbackFactory testFallbackFactory() { |
||||
return new TestFallbackFactory(); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class MyCircuitBreaker implements CircuitBreaker { |
||||
|
||||
AtomicBoolean runWasCalled = new AtomicBoolean(); |
||||
|
||||
@Override |
||||
public <T> T run(Supplier<T> toRun) { |
||||
this.runWasCalled.set(true); |
||||
return toRun.get(); |
||||
} |
||||
|
||||
@Override |
||||
public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) { |
||||
try { |
||||
return run(toRun); |
||||
} |
||||
catch (Throwable throwable) { |
||||
return fallback.apply(throwable); |
||||
} |
||||
} |
||||
|
||||
public void clear() { |
||||
this.runWasCalled.set(false); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue