Skip to content

Commit 5ea121e

Browse files
chihyuh3Chih-Yu Huang
authored andcommitted
fix: make RecoverAnnotationRecoveryHandler deterministic
When multiple @recover candidates exist, choose the most specific and deterministic handler instead of relying on reflection order. Signed-off-by: Chih-Yu Huang <[email protected]>
1 parent 671f208 commit 5ea121e

File tree

1 file changed

+68
-40
lines changed

1 file changed

+68
-40
lines changed

src/main/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandler.java

Lines changed: 68 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@
1919
import java.lang.reflect.Method;
2020
import java.lang.reflect.ParameterizedType;
2121
import java.lang.reflect.Type;
22+
import java.util.Arrays;
23+
import java.util.List;
24+
import java.util.ArrayList;
25+
import java.util.Comparator;
2226
import java.util.HashMap;
27+
import java.util.LinkedHashMap;
2328
import java.util.Map;
29+
import java.util.stream.Collectors;
2430

2531
import org.springframework.classify.SubclassClassifier;
2632
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -55,12 +61,13 @@
5561
* @author Gianluca Medici
5662
* @author Lijinliang
5763
* @author Yanming Zhou
64+
* @author Chih-Yu Huang
5865
*/
5966
public class RecoverAnnotationRecoveryHandler<T> implements MethodInvocationRecoverer<T> {
6067

6168
private final SubclassClassifier<Throwable, Method> classifier = new SubclassClassifier<>();
6269

63-
private final Map<Method, SimpleMetadata> methods = new HashMap<>();
70+
private final Map<Method, SimpleMetadata> methods = new LinkedHashMap<>();
6471

6572
private final Object target;
6673

@@ -73,7 +80,8 @@ public RecoverAnnotationRecoveryHandler(Object target, Method method) {
7380

7481
@Override
7582
public T recover(Object[] args, Throwable cause) {
76-
Method method = findClosestMatch(args, cause.getClass());
83+
Class<? extends Throwable> causeType = (cause == null) ? null : cause.getClass();
84+
Method method = findClosestMatch(args, causeType);
7785
if (method == null) {
7886
throw new ExhaustedRetryException("Cannot locate recovery method", cause);
7987
}
@@ -111,48 +119,59 @@ private Method findMethodOnProxy(Method method, Object proxy) {
111119
}
112120
}
113121

114-
private Method findClosestMatch(Object[] args, Class<? extends Throwable> cause) {
115-
Method result = null;
116-
117-
if (!StringUtils.hasText(this.recoverMethodName)) {
118-
int min = Integer.MAX_VALUE;
119-
for (Map.Entry<Method, SimpleMetadata> entry : this.methods.entrySet()) {
120-
Method method = entry.getKey();
121-
SimpleMetadata meta = entry.getValue();
122-
Class<? extends Throwable> type = meta.getType();
123-
if (type == null) {
124-
type = Throwable.class;
122+
private Method findClosestMatch(Object[] args, Class<? extends Throwable> causeType) {
123+
List<Method> methodsWithThrowable = new ArrayList<>();
124+
List<Method> methodsWithoutThrowable = new ArrayList<>();
125+
126+
for (Method method : this.methods.keySet()) {
127+
SimpleMetadata meta = this.methods.get(method);
128+
if (meta.getType() != null) {
129+
methodsWithThrowable.add(method);
130+
}
131+
else {
132+
methodsWithoutThrowable.add(method);
133+
}
134+
}
135+
136+
Method bestMatch = null;
137+
138+
if (causeType != null) {
139+
int minDistance = Integer.MAX_VALUE;
140+
for (Method method : methodsWithThrowable) {
141+
if (!compareParameters(args, method.getParameterCount(), method.getParameterTypes(), false)) {
142+
continue;
125143
}
126-
if (type.isAssignableFrom(cause)) {
127-
int distance = calculateDistance(cause, type);
128-
if (distance < min) {
129-
min = distance;
130-
result = method;
131-
}
132-
else if (distance == min) {
133-
boolean parametersMatch = compareParameters(args, meta.getArgCount(),
134-
method.getParameterTypes(), false);
135-
if (parametersMatch) {
136-
result = method;
137-
}
144+
145+
SimpleMetadata meta = this.methods.get(method);
146+
Class<? extends Throwable> methodExceptionType = meta.getType();
147+
int distance = calculateDistance(causeType, methodExceptionType);
148+
149+
if (bestMatch == null) {
150+
bestMatch = method;
151+
minDistance = distance;
152+
}
153+
else if (distance < minDistance) {
154+
minDistance = distance;
155+
bestMatch = method;
156+
}
157+
else if (distance == minDistance) {
158+
if (method.getParameterCount() > bestMatch.getParameterCount()) {
159+
bestMatch = method;
138160
}
139161
}
140162
}
141163
}
142-
else {
143-
for (Map.Entry<Method, SimpleMetadata> entry : this.methods.entrySet()) {
144-
Method method = entry.getKey();
145-
if (method.getName().equals(this.recoverMethodName)) {
146-
SimpleMetadata meta = entry.getValue();
147-
if ((meta.type == null || meta.type.isAssignableFrom(cause))
148-
&& compareParameters(args, meta.getArgCount(), method.getParameterTypes(), true)) {
149-
result = method;
150-
break;
151-
}
164+
165+
if (bestMatch == null) {
166+
for (Method method : methodsWithoutThrowable) {
167+
if (compareParameters(args, method.getParameterCount(), method.getParameterTypes(), false)) {
168+
bestMatch = method;
169+
break;
152170
}
153171
}
154172
}
155-
return result;
173+
174+
return bestMatch;
156175
}
157176

158177
private int calculateDistance(Class<? extends Throwable> cause, Class<? extends Throwable> type) {
@@ -175,6 +194,9 @@ private boolean compareParameters(Object[] args, int argCount, Class<?>[] parame
175194
for (int i = startingIndex; i < parameterTypes.length; i++) {
176195
final Object argument = i - startingIndex < args.length ? args[i - startingIndex] : null;
177196
if (argument == null) {
197+
if (parameterTypes[i].isPrimitive()) {
198+
return false;
199+
}
178200
continue;
179201
}
180202
Class<?> parameterType = parameterTypes[i];
@@ -189,13 +211,19 @@ private boolean compareParameters(Object[] args, int argCount, Class<?>[] parame
189211
}
190212

191213
private void init(final Object target, Method method) {
192-
final Map<Class<? extends Throwable>, Method> types = new HashMap<>();
214+
final Map<Class<? extends Throwable>, Method> types = new LinkedHashMap<>();
193215
final Method failingMethod = method;
194216
Retryable retryable = AnnotatedElementUtils.findMergedAnnotation(method, Retryable.class);
195217
if (retryable != null) {
196218
this.recoverMethodName = retryable.recover();
197219
}
198-
ReflectionUtils.doWithMethods(target.getClass(), candidate -> {
220+
Method[] declared = target.getClass().getDeclaredMethods();
221+
Arrays.sort(declared, Comparator.comparing(Method::getName)
222+
.thenComparingInt(Method::getParameterCount)
223+
.thenComparing(
224+
m -> Arrays.stream(m.getParameterTypes()).map(Class::getName).collect(Collectors.joining(","))));
225+
226+
for (Method candidate : declared) {
199227
Recover recover = AnnotatedElementUtils.findMergedAnnotation(candidate, Recover.class);
200228
if (recover == null) {
201229
recover = findAnnotationOnTarget(target, candidate);
@@ -210,7 +238,7 @@ private void init(final Object target, Method method) {
210238
else if (recover != null && candidate.getReturnType().isAssignableFrom(failingMethod.getReturnType())) {
211239
putToMethodsMap(candidate, types);
212240
}
213-
});
241+
}
214242
this.classifier.setTypeMap(types);
215243
optionallyFilterMethodsBy(failingMethod.getReturnType());
216244
}
@@ -280,7 +308,7 @@ private Recover findAnnotationOnTarget(Object target, Method method) {
280308
}
281309

282310
private void optionallyFilterMethodsBy(Class<?> returnClass) {
283-
Map<Method, SimpleMetadata> filteredMethods = new HashMap<>();
311+
Map<Method, SimpleMetadata> filteredMethods = new LinkedHashMap<>();
284312
for (Method method : this.methods.keySet()) {
285313
if (method.getReturnType() == returnClass) {
286314
filteredMethods.put(method, this.methods.get(method));

0 commit comments

Comments
 (0)