Skip to content

Commit de3bfbe

Browse files
RomehRobWin
authored andcommitted
Added support for generice fallback method with just execption as a param plus some code refactoring (ReactiveX#457)
* change fallback package * Review comments * Review comments
1 parent a394d4c commit de3bfbe

File tree

10 files changed

+220
-140
lines changed

10 files changed

+220
-140
lines changed

resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspect.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@
1515
*/
1616
package io.github.resilience4j.bulkhead.configure;
1717

18-
import io.github.resilience4j.bulkhead.BulkheadRegistry;
19-
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
20-
import io.github.resilience4j.core.lang.Nullable;
21-
import io.github.resilience4j.fallback.FallbackDecorators;
22-
import io.github.resilience4j.fallback.FallbackMethod;
23-
import io.github.resilience4j.utils.AnnotationExtractor;
18+
import java.lang.reflect.Method;
19+
import java.util.List;
20+
import java.util.concurrent.CompletionException;
21+
import java.util.concurrent.CompletionStage;
22+
2423
import org.aspectj.lang.ProceedingJoinPoint;
2524
import org.aspectj.lang.annotation.Around;
2625
import org.aspectj.lang.annotation.Aspect;
@@ -32,10 +31,12 @@
3231
import org.springframework.core.Ordered;
3332
import org.springframework.util.StringUtils;
3433

35-
import java.lang.reflect.Method;
36-
import java.util.List;
37-
import java.util.concurrent.CompletionException;
38-
import java.util.concurrent.CompletionStage;
34+
import io.github.resilience4j.bulkhead.BulkheadRegistry;
35+
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
36+
import io.github.resilience4j.core.lang.Nullable;
37+
import io.github.resilience4j.fallback.FallbackDecorators;
38+
import io.github.resilience4j.fallback.FallbackMethod;
39+
import io.github.resilience4j.utils.AnnotationExtractor;
3940

4041
/**
4142
* This Spring AOP aspect intercepts all methods which are annotated with a {@link Bulkhead} annotation.
@@ -97,8 +98,7 @@ public Object bulkheadAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, @Nul
9798
if (StringUtils.isEmpty(bulkheadAnnotation.fallbackMethod())) {
9899
return proceed(proceedingJoinPoint, methodName, bulkhead, returnType);
99100
}
100-
101-
FallbackMethod fallbackMethod = new FallbackMethod(bulkheadAnnotation.fallbackMethod(), method, proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget());
101+
FallbackMethod fallbackMethod = FallbackMethod.create(bulkheadAnnotation.fallbackMethod(), method, proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget());
102102
return fallbackDecorators.decorate(fallbackMethod, () -> proceed(proceedingJoinPoint, methodName, bulkhead, returnType)).apply();
103103
}
104104

resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspect.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@
1515
*/
1616
package io.github.resilience4j.circuitbreaker.configure;
1717

18-
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
19-
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
20-
import io.github.resilience4j.core.lang.Nullable;
21-
import io.github.resilience4j.fallback.FallbackDecorators;
22-
import io.github.resilience4j.fallback.FallbackMethod;
23-
import io.github.resilience4j.utils.AnnotationExtractor;
18+
import java.lang.reflect.Method;
19+
import java.util.List;
20+
import java.util.concurrent.CompletionException;
21+
import java.util.concurrent.CompletionStage;
22+
2423
import org.aspectj.lang.ProceedingJoinPoint;
2524
import org.aspectj.lang.annotation.Around;
2625
import org.aspectj.lang.annotation.Aspect;
@@ -32,10 +31,12 @@
3231
import org.springframework.core.Ordered;
3332
import org.springframework.util.StringUtils;
3433

35-
import java.lang.reflect.Method;
36-
import java.util.List;
37-
import java.util.concurrent.CompletionException;
38-
import java.util.concurrent.CompletionStage;
34+
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
35+
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
36+
import io.github.resilience4j.core.lang.Nullable;
37+
import io.github.resilience4j.fallback.FallbackDecorators;
38+
import io.github.resilience4j.fallback.FallbackMethod;
39+
import io.github.resilience4j.utils.AnnotationExtractor;
3940

4041
/**
4142
* This Spring AOP aspect intercepts all methods which are annotated with a {@link CircuitBreaker} annotation.
@@ -96,8 +97,7 @@ public Object circuitBreakerAroundAdvice(ProceedingJoinPoint proceedingJoinPoint
9697
if (StringUtils.isEmpty(circuitBreakerAnnotation.fallbackMethod())) {
9798
return proceed(proceedingJoinPoint, methodName, circuitBreaker, returnType);
9899
}
99-
100-
FallbackMethod fallbackMethod = new FallbackMethod(circuitBreakerAnnotation.fallbackMethod(), method, proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget());
100+
FallbackMethod fallbackMethod = FallbackMethod.create(circuitBreakerAnnotation.fallbackMethod(), method, proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget());
101101
return fallbackDecorators.decorate(fallbackMethod, () -> proceed(proceedingJoinPoint, methodName, circuitBreaker, returnType)).apply();
102102
}
103103

resilience4j-spring/src/main/java/io/github/resilience4j/fallback/CompletionStageFallbackDecorator.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
*/
1616
package io.github.resilience4j.fallback;
1717

18-
import io.vavr.CheckedFunction0;
19-
2018
import java.util.concurrent.CompletableFuture;
2119
import java.util.concurrent.CompletionStage;
2220

21+
import io.vavr.CheckedFunction0;
22+
2323
/**
2424
* fallbackMethod decorator for {@link CompletionStage}
2525
*/
@@ -32,7 +32,7 @@ public boolean supports(Class<?> target) {
3232

3333
@SuppressWarnings("unchecked")
3434
@Override
35-
public CheckedFunction0<Object> decorate(FallbackMethod recoveryMethod, CheckedFunction0<Object> supplier) {
35+
public CheckedFunction0<Object> decorate(FallbackMethod fallbackMethod, CheckedFunction0<Object> supplier) {
3636
return supplier.andThen(request -> {
3737
CompletionStage completionStage = (CompletionStage) request;
3838

@@ -41,7 +41,7 @@ public CheckedFunction0<Object> decorate(FallbackMethod recoveryMethod, CheckedF
4141
completionStage.whenComplete((result, throwable) -> {
4242
if (throwable != null) {
4343
try {
44-
((CompletionStage) recoveryMethod.recover((Throwable) throwable))
44+
((CompletionStage) fallbackMethod.fallback((Throwable) throwable))
4545
.whenComplete((recoveryResult, recoveryThrowable) -> {
4646
if (recoveryThrowable != null) {
4747
promise.completeExceptionally((Throwable) recoveryThrowable);

resilience4j-spring/src/main/java/io/github/resilience4j/fallback/DefaultFallbackDecorator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public CheckedFunction0<Object> decorate(FallbackMethod recoveryMethod, CheckedF
3333
try {
3434
return supplier.apply();
3535
} catch (Throwable throwable) {
36-
return recoveryMethod.recover(throwable);
36+
return recoveryMethod.fallback(throwable);
3737
}
3838
};
3939
}

resilience4j-spring/src/main/java/io/github/resilience4j/fallback/FallbackMethod.java

Lines changed: 91 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Kyuhyen Hwang
2+
* Copyright 2019 Kyuhyen Hwang, Mahmoud Romeh
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,28 +15,29 @@
1515
*/
1616
package io.github.resilience4j.fallback;
1717

18-
import io.github.resilience4j.core.lang.Nullable;
19-
import org.springframework.util.ConcurrentReferenceHashMap;
20-
import org.springframework.util.ReflectionUtils;
21-
import org.springframework.util.StringUtils;
22-
2318
import java.lang.reflect.InvocationTargetException;
2419
import java.lang.reflect.Method;
2520
import java.util.Arrays;
2621
import java.util.HashMap;
2722
import java.util.Map;
2823

24+
import org.springframework.util.ConcurrentReferenceHashMap;
25+
import org.springframework.util.ReflectionUtils;
26+
import org.springframework.util.StringUtils;
27+
28+
import io.github.resilience4j.core.lang.Nullable;
29+
2930
/**
30-
* Reflection utility for invoking a fallback method. A fallback method should have the same return type and parameter types of original method but the last additional parameter.
31-
* The last additional parameter should be a subclass of {@link Throwable}. When {@link FallbackMethod#recover(Throwable)} is invoked, {@link Throwable} will be passed to that last parameter.
32-
* If there are multiple fallback methods, one of the methods that has most closest superclass parameter of thrown object will be invoked.
31+
* Reflection utility for invoking a fallback method. Fallback method should have same return type and parameter types of original method but the last additional parameter.
32+
* The last additional parameter should be a subclass of {@link Throwable}. When {@link FallbackMethod#fallback(Throwable)} is invoked, {@link Throwable} will be passed to that last parameter.
33+
* If there are multiple fallback method, one of the methods that has most closest superclass parameter of thrown object will be invoked.
3334
* <pre>
3435
* For example, there are two fallback methods
3536
* {@code
3637
* String fallbackMethod(String parameter, RuntimeException exception)
3738
* String fallbackMethod(String parameter, IllegalArgumentException exception)
3839
* }
39-
* and if try to recover from {@link NumberFormatException}, {@code String fallbackMethod(String parameter, IllegalArgumentException exception)} will be invoked.
40+
* and if try to fallback from {@link NumberFormatException}, {@code String fallbackMethod(String parameter, IllegalArgumentException exception)} will be invoked.
4041
* </pre>
4142
*/
4243
public class FallbackMethod {
@@ -49,37 +50,49 @@ public class FallbackMethod {
4950
/**
5051
* create a fallbackMethod method.
5152
*
52-
* @param recoveryMethodName fallbackMethod method name
53-
* @param originalMethod will be used for checking return type and parameter types of the fallbackMethod method
54-
* @param args arguments those were passed to the original method. They will be passed to the fallbackMethod method.
55-
* @param target target object the fallbackMethod method will be invoked
56-
* @throws NoSuchMethodException will be thrown, if fallbackMethod method is not found
53+
* @param recoveryMethods configured and found recovery methods for this invocation
54+
* @param originalMethodReturnType the return type of the original source method
55+
* @param args arguments those were passed to the original method. They will be passed to the fallbackMethod method.
56+
* @param target target object the fallbackMethod method will be invoked
5757
*/
58-
public FallbackMethod(String recoveryMethodName, Method originalMethod, Object[] args, Object target) throws NoSuchMethodException {
58+
private FallbackMethod(Map<Class<?>, Method> recoveryMethods, Class<?> originalMethodReturnType, Object[] args, Object target) {
59+
60+
this.recoveryMethods = recoveryMethods;
61+
this.args = args;
62+
this.target = target;
63+
this.returnType = originalMethodReturnType;
64+
}
65+
66+
/**
67+
* @param fallbackMethodName the configured recovery method name
68+
* @param originalMethod the original method which has fallback method configured
69+
* @param args the original method arguments
70+
* @param target the target class that own the original method and recovery method
71+
* @return FallbackMethod instance
72+
*/
73+
public static FallbackMethod create(String fallbackMethodName, Method originalMethod, Object[] args, Object target) throws NoSuchMethodException {
74+
5975
Class<?>[] params = originalMethod.getParameterTypes();
6076
Class<?> originalReturnType = originalMethod.getReturnType();
6177

62-
Map<Class<?>, Method> methods = extractMethods(recoveryMethodName, params, originalReturnType, target.getClass());
78+
Map<Class<?>, Method> methods = extractMethods(fallbackMethodName, params, originalReturnType, target.getClass());
6379

6480
if (methods.isEmpty()) {
65-
throw new NoSuchMethodException(String.format("%s %s.%s(%s,%s)", originalReturnType, target.getClass(), recoveryMethodName, StringUtils.arrayToDelimitedString(params, ","), Throwable.class));
81+
throw new NoSuchMethodException(String.format("%s %s.%s(%s,%s)", originalReturnType, target.getClass(), fallbackMethodName, StringUtils.arrayToDelimitedString(params, ","), Throwable.class));
6682
}
83+
return new FallbackMethod(methods, originalReturnType, args, target);
6784

68-
this.recoveryMethods = methods;
69-
this.args = args;
70-
this.target = target;
71-
this.returnType = originalReturnType;
7285
}
7386

7487
/**
75-
* try to recover from {@link Throwable}
88+
* try to fallback from {@link Throwable}
7689
*
77-
* @param thrown {@link Throwable} that should be recover
90+
* @param thrown {@link Throwable} that should be fallback
7891
* @return recovered value
7992
* @throws Throwable if throwable is unrecoverable, throwable will be thrown
8093
*/
8194
@Nullable
82-
public Object recover(Throwable thrown) throws Throwable {
95+
public Object fallback(Throwable thrown) throws Throwable {
8396
if (recoveryMethods.size() == 1) {
8497
Map.Entry<Class<?>, Method> entry = recoveryMethods.entrySet().iterator().next();
8598
if (entry.getKey().isAssignableFrom(thrown.getClass())) {
@@ -111,31 +124,48 @@ public Class<?> getReturnType() {
111124
return returnType;
112125
}
113126

114-
private Object invoke(Method recovery, Throwable throwable) throws IllegalAccessException, InvocationTargetException {
115-
boolean accessible = recovery.isAccessible();
127+
/**
128+
* invoke the fallback method logic
129+
*
130+
* @param fallback fallback method
131+
* @param throwable the thrown exception
132+
* @return the result object if any
133+
* @throws IllegalAccessException
134+
* @throws InvocationTargetException
135+
*/
136+
private Object invoke(Method fallback, Throwable throwable) throws IllegalAccessException, InvocationTargetException {
137+
boolean accessible = fallback.isAccessible();
116138
try {
117139
if (!accessible) {
118-
ReflectionUtils.makeAccessible(recovery);
140+
ReflectionUtils.makeAccessible(fallback);
119141
}
120-
121142
if (args.length != 0) {
143+
if (args.length == 1 && Throwable.class.isAssignableFrom(fallback.getParameterTypes()[0])) {
144+
return fallback.invoke(target, throwable);
145+
}
122146
Object[] newArgs = Arrays.copyOf(args, args.length + 1);
123147
newArgs[args.length] = throwable;
124148

125-
return recovery.invoke(target, newArgs);
149+
return fallback.invoke(target, newArgs);
126150

127151
} else {
128-
return recovery.invoke(target, throwable);
152+
return fallback.invoke(target, throwable);
129153
}
130154
} finally {
131155
if (!accessible) {
132-
recovery.setAccessible(false);
156+
fallback.setAccessible(false);
133157
}
134158
}
135159
}
136-
137-
private static Map<Class<?>, Method> extractMethods(String recoveryMethodName, Class<?>[] params, Class<?> originalReturnType, Class<?> targetClass) {
138-
MethodMeta methodMeta = new MethodMeta(recoveryMethodName, params, originalReturnType, targetClass);
160+
/**
161+
* @param fallbackMethodName fallback method name
162+
* @param params original method parameters
163+
* @param originalReturnType original method return type
164+
* @param targetClass the owner class
165+
* @return Map<Class < ?>, Method> map of all configure fallback methods for the original method that match the fallback method name
166+
*/
167+
private static Map<Class<?>, Method> extractMethods(String fallbackMethodName, Class<?>[] params, Class<?> originalReturnType, Class<?> targetClass) {
168+
MethodMeta methodMeta = new MethodMeta(fallbackMethodName, params, originalReturnType, targetClass);
139169
Map<Class<?>, Method> cachedMethods = RECOVERY_METHODS_CACHE.get(methodMeta);
140170

141171
if (cachedMethods != null) {
@@ -146,29 +176,42 @@ private static Map<Class<?>, Method> extractMethods(String recoveryMethodName, C
146176

147177
ReflectionUtils.doWithMethods(targetClass, method -> {
148178
Class<?>[] recoveryParams = method.getParameterTypes();
179+
if (methods.get(recoveryParams[recoveryParams.length - 1]) != null) {
180+
throw new IllegalStateException("You have more that one fallback method that cover the same exception type " + recoveryParams[recoveryParams.length - 1].getName());
181+
}
149182
methods.put(recoveryParams[recoveryParams.length - 1], method);
150183
}, method -> {
151-
if (!method.getName().equals(recoveryMethodName) || method.getParameterCount() != params.length + 1) {
152-
return false;
153-
}
154-
if (!originalReturnType.isAssignableFrom(method.getReturnType())) {
155-
return false;
156-
}
157-
158-
Class[] targetParams = method.getParameterTypes();
159-
for (int i = 0; i < params.length; i++) {
160-
if (params[i] != targetParams[i]) {
184+
if (method.getParameterCount() == 1) {
185+
if (!method.getName().equals(fallbackMethodName) || !originalReturnType.isAssignableFrom(method.getReturnType())) {
186+
return false;
187+
}
188+
return Throwable.class.isAssignableFrom(method.getParameterTypes()[0]);
189+
} else {
190+
if (!method.getName().equals(fallbackMethodName) || method.getParameterCount() != params.length + 1) {
191+
return false;
192+
}
193+
if (!originalReturnType.isAssignableFrom(method.getReturnType())) {
161194
return false;
162195
}
196+
197+
Class[] targetParams = method.getParameterTypes();
198+
for (int i = 0; i < params.length; i++) {
199+
if (params[i] != targetParams[i]) {
200+
return false;
201+
}
202+
}
203+
return Throwable.class.isAssignableFrom(targetParams[params.length]);
163204
}
164205

165-
return Throwable.class.isAssignableFrom(targetParams[params.length]);
166206
});
167207

168208
RECOVERY_METHODS_CACHE.putIfAbsent(methodMeta, methods);
169209
return methods;
170210
}
171211

212+
/**
213+
* fallback method cache lookup key
214+
*/
172215
private static class MethodMeta {
173216
final String recoveryMethodName;
174217
final Class<?>[] params;
@@ -195,7 +238,7 @@ public boolean equals(Object o) {
195238

196239
@Override
197240
public int hashCode() {
198-
return targetClass.getName().hashCode() ^ recoveryMethodName.hashCode();
241+
return targetClass.getName().hashCode() ^ recoveryMethodName.hashCode();
199242
}
200243
}
201-
}
244+
}

0 commit comments

Comments
 (0)