Skip to content

Commit d95c9b3

Browse files
committed
Add generic check for function return type
Fixes: [#215](#215) Signed-off-by: deepesh-verma <[email protected]>
1 parent 868d205 commit d95c9b3

File tree

2 files changed

+272
-14
lines changed

2 files changed

+272
-14
lines changed

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

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.retry.annotation;
1818

1919
import java.lang.reflect.Method;
20+
import java.lang.reflect.ParameterizedType;
21+
import java.lang.reflect.Type;
2022
import java.util.HashMap;
2123
import java.util.Map;
2224

@@ -173,27 +175,67 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess
173175
if (recover == null) {
174176
recover = findAnnotationOnTarget(target, method);
175177
}
176-
if (recover != null && method.getReturnType().isAssignableFrom(failingMethod.getReturnType())) {
177-
Class<?>[] parameterTypes = method.getParameterTypes();
178-
if (parameterTypes.length > 0 && Throwable.class.isAssignableFrom(parameterTypes[0])) {
179-
@SuppressWarnings("unchecked")
180-
Class<? extends Throwable> type = (Class<? extends Throwable>) parameterTypes[0];
181-
types.put(type, method);
182-
RecoverAnnotationRecoveryHandler.this.methods.put(method,
183-
new SimpleMetadata(parameterTypes.length, type));
184-
}
185-
else {
186-
RecoverAnnotationRecoveryHandler.this.classifier.setDefaultValue(method);
187-
RecoverAnnotationRecoveryHandler.this.methods.put(method,
188-
new SimpleMetadata(parameterTypes.length, null));
178+
if (recover != null && failingMethod.getGenericReturnType() instanceof ParameterizedType
179+
&& method.getGenericReturnType() instanceof ParameterizedType) {
180+
if (isParameterizedTypeAssignable((ParameterizedType) method.getGenericReturnType(),
181+
(ParameterizedType) failingMethod.getGenericReturnType())) {
182+
putToMethodsMap(method, types);
189183
}
190184
}
185+
else if (recover != null && method.getReturnType().isAssignableFrom(failingMethod.getReturnType())) {
186+
putToMethodsMap(method, types);
187+
}
191188
}
192189
});
193190
this.classifier.setTypeMap(types);
194191
optionallyFilterMethodsBy(failingMethod.getReturnType());
195192
}
196193

194+
/**
195+
* Returns {@code true} if the input methodReturnType is a direct match of the
196+
* failingMethodReturnType. Takes nested generics into consideration as well, while
197+
* deciding a match.
198+
* @param methodReturnType
199+
* @param failingMethodReturnType
200+
* @return
201+
*/
202+
private boolean isParameterizedTypeAssignable(ParameterizedType methodReturnType,
203+
ParameterizedType failingMethodReturnType) {
204+
Type[] methodActualArgs = methodReturnType.getActualTypeArguments();
205+
Type[] failingMethodActualArgs = failingMethodReturnType.getActualTypeArguments();
206+
if (methodActualArgs.length != failingMethodActualArgs.length) {
207+
return false;
208+
}
209+
int startingIndex = 0;
210+
for (int i = startingIndex; i < methodActualArgs.length; i++) {
211+
Type methodArgType = methodActualArgs[i];
212+
Type failingMethodArgType = failingMethodActualArgs[i];
213+
if (methodArgType instanceof ParameterizedType && failingMethodArgType instanceof ParameterizedType) {
214+
return isParameterizedTypeAssignable((ParameterizedType) methodArgType,
215+
(ParameterizedType) failingMethodArgType);
216+
}
217+
if (methodArgType instanceof Class && failingMethodArgType instanceof Class
218+
&& !failingMethodArgType.equals(methodArgType)) {
219+
return false;
220+
}
221+
}
222+
return true;
223+
}
224+
225+
private void putToMethodsMap(Method method, Map<Class<? extends Throwable>, Method> types) {
226+
Class<?>[] parameterTypes = method.getParameterTypes();
227+
if (parameterTypes.length > 0 && Throwable.class.isAssignableFrom(parameterTypes[0])) {
228+
@SuppressWarnings("unchecked")
229+
Class<? extends Throwable> type = (Class<? extends Throwable>) parameterTypes[0];
230+
types.put(type, method);
231+
RecoverAnnotationRecoveryHandler.this.methods.put(method, new SimpleMetadata(parameterTypes.length, type));
232+
}
233+
else {
234+
RecoverAnnotationRecoveryHandler.this.classifier.setDefaultValue(method);
235+
RecoverAnnotationRecoveryHandler.this.methods.put(method, new SimpleMetadata(parameterTypes.length, null));
236+
}
237+
}
238+
197239
private Recover findAnnotationOnTarget(Object target, Method method) {
198240
try {
199241
Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());

src/test/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandlerTests.java

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,19 @@
1818

1919
import java.lang.reflect.Method;
2020
import java.util.ArrayList;
21+
import java.util.Collections;
2122
import java.util.List;
23+
import java.util.Map;
2224

2325
import org.junit.Rule;
2426
import org.junit.Test;
2527
import org.junit.rules.ExpectedException;
2628

2729
import org.springframework.retry.ExhaustedRetryException;
30+
import org.springframework.util.CollectionUtils;
2831
import org.springframework.util.ReflectionUtils;
2932

30-
import static org.junit.Assert.assertEquals;
33+
import static org.junit.Assert.*;
3134

3235
/**
3336
* @author Dave Syer
@@ -107,6 +110,106 @@ public void parentReturnTypeRecoverMethod() {
107110

108111
}
109112

113+
@Test
114+
public void genericReturnStringValueTypeParentThrowableRecoverMethod() {
115+
116+
RecoverAnnotationRecoveryHandler<?> handler = new RecoverAnnotationRecoveryHandler<List<String>>(
117+
new GenericReturnTypeRecover(),
118+
ReflectionUtils.findMethod(GenericReturnTypeRecover.class, "foo", String.class));
119+
120+
@SuppressWarnings("unchecked")
121+
Map<String, String> recoverResponseMap = (Map<String, String>) handler.recover(new Object[] { "Aldo" },
122+
new RuntimeException("Planned"));
123+
assertFalse(CollectionUtils.isEmpty(recoverResponseMap));
124+
assertEquals("fooRecoverValue1", recoverResponseMap.get("foo"));
125+
}
126+
127+
@Test
128+
public void genericReturnStringValueTypeChildThrowableRecoverMethod() {
129+
130+
RecoverAnnotationRecoveryHandler<?> handler = new RecoverAnnotationRecoveryHandler<List<String>>(
131+
new GenericReturnTypeRecover(),
132+
ReflectionUtils.findMethod(GenericReturnTypeRecover.class, "foo", String.class));
133+
134+
@SuppressWarnings("unchecked")
135+
Map<String, String> recoverResponseMap = (Map<String, String>) handler.recover(new Object[] { "Aldo" },
136+
new IllegalStateException("Planned"));
137+
assertFalse(CollectionUtils.isEmpty(recoverResponseMap));
138+
assertEquals("fooRecoverValue2", recoverResponseMap.get("foo"));
139+
}
140+
141+
@Test
142+
public void genericReturnOneValueTypeRecoverMethod() {
143+
144+
RecoverAnnotationRecoveryHandler<?> handler = new RecoverAnnotationRecoveryHandler<List<String>>(
145+
new GenericReturnTypeRecover(),
146+
ReflectionUtils.findMethod(GenericReturnTypeRecover.class, "bar", String.class));
147+
148+
@SuppressWarnings("unchecked")
149+
Map<String, GenericReturnTypeRecover.One> recoverResponseMap = (Map<String, GenericReturnTypeRecover.One>) handler
150+
.recover(new Object[] { "Aldo" }, new RuntimeException("Planned"));
151+
assertFalse(CollectionUtils.isEmpty(recoverResponseMap));
152+
assertNotNull(recoverResponseMap.get("bar"));
153+
assertEquals("barRecoverValue", recoverResponseMap.get("bar").name);
154+
}
155+
156+
@Test
157+
public void genericSpecifiedReturnTypeRecoverMethod() {
158+
RecoverAnnotationRecoveryHandler<?> fooHandler = new RecoverAnnotationRecoveryHandler<Integer>(
159+
new GenericInheritanceReturnTypeRecover(),
160+
ReflectionUtils.findMethod(GenericInheritanceReturnTypeRecover.class, "foo", String.class));
161+
@SuppressWarnings("unchecked")
162+
Map<String, Integer> recoverResponseMapRe = (Map<String, Integer>) fooHandler.recover(new Object[] { "Aldo" },
163+
new RuntimeException("Planned"));
164+
assertEquals(1, recoverResponseMapRe.get("foo").intValue());
165+
@SuppressWarnings("unchecked")
166+
Map<String, Integer> recoverResponseMapIse = (Map<String, Integer>) fooHandler.recover(new Object[] { "Aldo" },
167+
new IllegalStateException("Planned"));
168+
assertEquals(2, recoverResponseMapIse.get("foo").intValue());
169+
}
170+
171+
/**
172+
* Even if there are @Recover methods with narrower generic return type, the one with
173+
* direct match should get called
174+
*/
175+
@Test
176+
public void genericChildReturnTypeRecoverMethod() {
177+
RecoverAnnotationRecoveryHandler<?> barHandler = new RecoverAnnotationRecoveryHandler<Double>(
178+
new GenericInheritanceReturnTypeRecover(),
179+
ReflectionUtils.findMethod(GenericInheritanceReturnTypeRecover.class, "bar", String.class));
180+
@SuppressWarnings("unchecked")
181+
Map<String, Double> recoverResponseMapRe = (Map<String, Double>) barHandler.recover(new Object[] { "Aldo" },
182+
new RuntimeException("Planned"));
183+
assertEquals(0.2, recoverResponseMapRe.get("bar"), 0.0);
184+
}
185+
186+
@Test
187+
public void genericMapListIntegerReturnTypeRecoverMethod() {
188+
RecoverAnnotationRecoveryHandler<?> fooHandler = new RecoverAnnotationRecoveryHandler<Integer>(
189+
new NestedGenericInheritanceReturnTypeRecover(),
190+
ReflectionUtils.findMethod(NestedGenericInheritanceReturnTypeRecover.class, "foo", String.class));
191+
@SuppressWarnings("unchecked")
192+
Map<String, Map<String, Map<Integer, String>>> recoverResponseMapRe = (Map<String, Map<String, Map<Integer, String>>>) fooHandler
193+
.recover(new Object[] { "Aldo" }, new RuntimeException("Planned"));
194+
assertEquals("fooRecoverReValue", recoverResponseMapRe.get("foo").get("foo").get(0));
195+
@SuppressWarnings("unchecked")
196+
Map<String, Map<String, Map<Integer, String>>> recoverResponseMapIe = (Map<String, Map<String, Map<Integer, String>>>) fooHandler
197+
.recover(new Object[] { "Aldo" }, new IllegalStateException("Planned"));
198+
assertEquals("fooRecoverIeValue", recoverResponseMapIe.get("foo").get("foo").get(0));
199+
}
200+
201+
@Test
202+
public void genericMapListDoubleReturnTypeRecoverMethod() {
203+
RecoverAnnotationRecoveryHandler<?> barHandler = new RecoverAnnotationRecoveryHandler<Double>(
204+
new NestedGenericInheritanceReturnTypeRecover(),
205+
ReflectionUtils.findMethod(NestedGenericInheritanceReturnTypeRecover.class, "bar", String.class));
206+
@SuppressWarnings("unchecked")
207+
Map<String, Map<String, Map<Number, String>>> recoverResponseMapRe = (Map<String, Map<String, Map<Number, String>>>) barHandler
208+
.recover(new Object[] { "Aldo" }, new RuntimeException("Planned"));
209+
assertEquals("barRecoverNumberValue", recoverResponseMapRe.get("bar").get("bar").get(0.0));
210+
211+
}
212+
110213
@Test
111214
public void multipleQualifyingRecoverMethods() {
112215
Method foo = ReflectionUtils.findMethod(MultipleQualifyingRecovers.class, "foo", String.class);
@@ -293,6 +396,119 @@ public Number quux(RuntimeException re, String name) {
293396

294397
}
295398

399+
protected static class GenericReturnTypeRecover {
400+
401+
private static class One {
402+
403+
String name;
404+
405+
public One(String name) {
406+
this.name = name;
407+
}
408+
409+
}
410+
411+
@Retryable
412+
public Map<String, String> foo(String name) {
413+
return Collections.singletonMap("foo", "fooValue");
414+
}
415+
416+
@Retryable
417+
public Map<String, One> bar(String name) {
418+
return Collections.singletonMap("bar", new One("barValue"));
419+
}
420+
421+
@Recover
422+
public Map<String, String> fooRecoverRe(RuntimeException re, String name) {
423+
return Collections.singletonMap("foo", "fooRecoverValue1");
424+
}
425+
426+
@Recover
427+
public Map<String, String> fooRecoverIe(IllegalStateException re, String name) {
428+
return Collections.singletonMap("foo", "fooRecoverValue2");
429+
}
430+
431+
@Recover
432+
public Map<String, One> barRecover(RuntimeException re, String name) {
433+
return Collections.singletonMap("bar", new One("barRecoverValue"));
434+
}
435+
436+
}
437+
438+
protected static class GenericInheritanceReturnTypeRecover {
439+
440+
@Retryable
441+
public Map<String, Integer> foo(String name) {
442+
return Collections.singletonMap("foo", 0);
443+
}
444+
445+
@Retryable
446+
public Map<String, Number> bar(String name) {
447+
return Collections.singletonMap("bar", (Number) 0.0);
448+
}
449+
450+
@Recover
451+
public Map<String, Integer> fooRecoverRe(RuntimeException re, String name) {
452+
return Collections.singletonMap("foo", 1);
453+
}
454+
455+
@Recover
456+
public Map<String, Integer> fooRecoverIe(IllegalStateException re, String name) {
457+
return Collections.singletonMap("foo", 2);
458+
}
459+
460+
@Recover
461+
public Map<String, Double> barRecoverDouble(RuntimeException re, String name) {
462+
return Collections.singletonMap("bar", 0.1);
463+
}
464+
465+
@Recover
466+
public Map<String, Number> barRecoverNumber(RuntimeException re, String name) {
467+
return Collections.singletonMap("bar", (Number) 0.2);
468+
}
469+
470+
}
471+
472+
protected static class NestedGenericInheritanceReturnTypeRecover {
473+
474+
@Retryable
475+
public Map<String, Map<String, Map<Integer, String>>> foo(String name) {
476+
return Collections.singletonMap("foo",
477+
Collections.singletonMap("foo", Collections.singletonMap(0, "fooValue")));
478+
}
479+
480+
@Retryable
481+
public Map<String, Map<String, Map<Number, String>>> bar(String name) {
482+
return Collections.singletonMap("bar",
483+
Collections.singletonMap("bar", Collections.singletonMap((Number) 0.0, "barValue")));
484+
}
485+
486+
@Recover
487+
public Map<String, Map<String, Map<Integer, String>>> fooRecoverRe(RuntimeException re, String name) {
488+
return Collections.singletonMap("foo",
489+
Collections.singletonMap("foo", Collections.singletonMap(0, "fooRecoverReValue")));
490+
}
491+
492+
@Recover
493+
public Map<String, Map<String, Map<Integer, String>>> fooRecoverIe(IllegalStateException re, String name) {
494+
return Collections.singletonMap("foo",
495+
Collections.singletonMap("foo", Collections.singletonMap(0, "fooRecoverIeValue")));
496+
}
497+
498+
@Recover
499+
public Map<String, Map<String, Map<Number, String>>> barRecoverNumber(RuntimeException re, String name) {
500+
return Collections.singletonMap("bar",
501+
Collections.singletonMap("bar", Collections.singletonMap((Number) 0.0, "barRecoverNumberValue")));
502+
}
503+
504+
@Recover
505+
public Map<String, Map<String, Map<Double, String>>> barRecoverDouble(RuntimeException re, String name) {
506+
return Collections.singletonMap("bar",
507+
Collections.singletonMap("bar", Collections.singletonMap(0.0, "barRecoverDoubleValue")));
508+
}
509+
510+
}
511+
296512
protected static class MultipleQualifyingRecoversNoThrowable {
297513

298514
@Retryable

0 commit comments

Comments
 (0)