Skip to content

Commit 2e7a73c

Browse files
gmediciartembilan
authored andcommitted
Allow composing @Retryable annotation
* fix(Retryable): allow composing `Retryable` annotation with recover argument annotated with @AliasFor by using `AnnotatedElementUtils.findMergedAnnotation` in `AnnotationAwareRetryOperationsInterceptor` * Updated README.md with example and explanation for custom annotation composition with @retryable Added author and fix import style. * Updated README.md removed version 2.0 mention on the Further customizations section # Conflicts: # src/main/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandler.java
1 parent 7063d36 commit 2e7a73c

File tree

3 files changed

+149
-5
lines changed

3 files changed

+149
-5
lines changed

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,94 @@ line to your `build.gradle` file:
595595
```
596596
runtime('org.aspectj:aspectjweaver:1.8.13')
597597
```
598+
### Further customizations
599+
600+
Starting from version 1.3.2 and later `@Retryable` annotation can be used in custom composed annotations to create your own annotations with predefined behaviour.
601+
For example if you discover you need two kinds of retry strategy, one for local services calls, and one for remote services calls, you could decide
602+
to create two custom annotations `@LocalRetryable` and `@RemoteRetryable` that differs in the retry strategy as well in the maximum number of retries.
603+
604+
To make custom annotation composition work properly you can use `@AliasFor` annotation, for example on the `recover` method, so that you can further extend the versatility of your custom annotations and allow the `recover` argument value
605+
to be picked up as if it was set on the `recover` method of the base `@Retryable` annotation.
606+
607+
Usage Example:
608+
```java
609+
@Service
610+
class Service {
611+
...
612+
613+
@LocalRetryable(include = TemporaryLocalException.class, recover = "service1Recovery")
614+
public List<Thing> service1(String str1, String str2){
615+
//... do something
616+
}
617+
618+
public List<Thing> service1Recovery(TemporaryLocalException ex,String str1, String str2){
619+
//... Error handling for service1
620+
}
621+
...
622+
623+
@RemoteRetryable(include = TemporaryRemoteException.class, recover = "service2Recovery")
624+
public List<Thing> service2(String str1, String str2){
625+
//... do something
626+
}
627+
628+
public List<Thing> service2Recovery(TemporaryRemoteException ex, String str1, String str2){
629+
//... Error handling for service2
630+
}
631+
...
632+
}
633+
```
634+
635+
```java
636+
@Target({ ElementType.METHOD, ElementType.TYPE })
637+
@Retention(RetentionPolicy.RUNTIME)
638+
@Retryable(maxAttempts = "3", backoff = @Backoff(delay = "500", maxDelay = "2000", random = true)
639+
)
640+
public @interface LocalRetryable {
641+
642+
@AliasFor(annotation = Retryable.class, attribute = "recover")
643+
String recover() default "";
644+
645+
@AliasFor(annotation = Retryable.class, attribute = "value")
646+
Class<? extends Throwable>[] value() default {};
647+
648+
@AliasFor(annotation = Retryable.class, attribute = "include")
649+
650+
Class<? extends Throwable>[] include() default {};
651+
652+
@AliasFor(annotation = Retryable.class, attribute = "exclude")
653+
Class<? extends Throwable>[] exclude() default {};
654+
655+
@AliasFor(annotation = Retryable.class, attribute = "label")
656+
String label() default "";
657+
658+
}
659+
```
660+
661+
```java
662+
@Target({ ElementType.METHOD, ElementType.TYPE })
663+
@Retention(RetentionPolicy.RUNTIME)
664+
@Documented
665+
@Retryable(maxAttempts = "5", backoff = @Backoff(delay = "1000", maxDelay = "30000", multiplier = "1.2", random = true)
666+
)
667+
public @interface RemoteRetryable {
668+
669+
@AliasFor(annotation = Retryable.class, attribute = "recover")
670+
String recover() default "";
671+
672+
@AliasFor(annotation = Retryable.class, attribute = "value")
673+
Class<? extends Throwable>[] value() default {};
674+
675+
@AliasFor(annotation = Retryable.class, attribute = "include")
676+
Class<? extends Throwable>[] include() default {};
677+
678+
@AliasFor(annotation = Retryable.class, attribute = "exclude")
679+
Class<? extends Throwable>[] exclude() default {};
680+
681+
@AliasFor(annotation = Retryable.class, attribute = "label")
682+
String label() default "";
683+
684+
}
685+
```
598686

599687
### XML Configuration
600688

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,13 @@
2323
import java.util.Map;
2424

2525
import org.springframework.classify.SubclassClassifier;
26-
import org.springframework.core.annotation.AnnotationUtils;
26+
import org.springframework.core.annotation.AnnotatedElementUtils;
2727
import org.springframework.retry.ExhaustedRetryException;
2828
import org.springframework.retry.RetryContext;
2929
import org.springframework.retry.interceptor.MethodInvocationRecoverer;
3030
import org.springframework.retry.support.RetrySynchronizationManager;
3131
import org.springframework.util.ClassUtils;
3232
import org.springframework.util.ReflectionUtils;
33-
import org.springframework.util.ReflectionUtils.MethodCallback;
3433
import org.springframework.util.StringUtils;
3534

3635
/**
@@ -53,6 +52,7 @@
5352
* @author Maksim Kita
5453
* @author Gary Russell
5554
* @author Artem Bilan
55+
* @author Gianluca Medici
5656
*/
5757
public class RecoverAnnotationRecoveryHandler<T> implements MethodInvocationRecoverer<T> {
5858

@@ -199,14 +199,14 @@ private boolean compareParameters(Object[] args, int argCount, Class<?>[] parame
199199
private void init(final Object target, Method method) {
200200
final Map<Class<? extends Throwable>, Method> types = new HashMap<Class<? extends Throwable>, Method>();
201201
final Method failingMethod = method;
202-
Retryable retryable = AnnotationUtils.findAnnotation(method, Retryable.class);
202+
Retryable retryable = AnnotatedElementUtils.findMergedAnnotation(method, Retryable.class);
203203
if (retryable != null) {
204204
this.recoverMethodName = retryable.recover();
205205
}
206206
ReflectionUtils.doWithMethods(target.getClass(), new MethodCallback() {
207207
@Override
208208
public void doWith(Method method) throws IllegalArgumentException {
209-
Recover recover = AnnotationUtils.findAnnotation(method, Recover.class);
209+
Recover recover = AnnotatedElementUtils.findMergedAnnotation(method, Recover.class);
210210
if (recover == null) {
211211
recover = findAnnotationOnTarget(target, method);
212212
}
@@ -276,7 +276,7 @@ private void putToMethodsMap(Method method, Map<Class<? extends Throwable>, Meth
276276
private Recover findAnnotationOnTarget(Object target, Method method) {
277277
try {
278278
Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
279-
return AnnotationUtils.findAnnotation(targetMethod, Recover.class);
279+
return AnnotatedElementUtils.findMergedAnnotation(targetMethod, Recover.class);
280280
}
281281
catch (Exception e) {
282282
return null;

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717
package org.springframework.retry.annotation;
1818

19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
1924
import java.lang.reflect.Method;
2025
import java.util.ArrayList;
2126
import java.util.Collections;
@@ -26,6 +31,7 @@
2631
import org.junit.Test;
2732
import org.junit.rules.ExpectedException;
2833

34+
import org.springframework.core.annotation.AliasFor;
2935
import org.springframework.retry.ExhaustedRetryException;
3036
import org.springframework.util.CollectionUtils;
3137
import org.springframework.util.ReflectionUtils;
@@ -40,6 +46,7 @@
4046
* @author Randell Callahan
4147
* @author Nathanaël Roberts
4248
* @author Maksim Kita
49+
* @Author Gianluca Medici
4350
*/
4451
public class RecoverAnnotationRecoveryHandlerTests {
4552

@@ -283,6 +290,14 @@ public void recoverByRetryableNameWithPrimitiveArgs() {
283290
assertEquals(2, handler.recover(new Object[] { 2 }, new RuntimeException("Planned")));
284291
}
285292

293+
@Test
294+
public void recoverByComposedRetryableAnnotationName() {
295+
Method foo = ReflectionUtils.findMethod(RecoverByComposedRetryableAnnotationName.class, "foo", String.class);
296+
RecoverAnnotationRecoveryHandler<?> handler = new RecoverAnnotationRecoveryHandler<Integer>(
297+
new RecoverByComposedRetryableAnnotationName(), foo);
298+
assertThat(handler.recover(new Object[] { "Kevin" }, new RuntimeException("Planned"))).isEqualTo(4);
299+
}
300+
286301
private static class InAccessibleRecover {
287302

288303
@Retryable
@@ -639,6 +654,23 @@ public int barRecover(Throwable throwable, String name) {
639654

640655
}
641656

657+
protected static class RecoverByComposedRetryableAnnotationName
658+
implements RecoverByComposedRetryableAnnotationNameInterface {
659+
660+
public int foo(String name) {
661+
return 0;
662+
}
663+
664+
public int fooRecover(Throwable throwable, String name) {
665+
return 1;
666+
}
667+
668+
public int barRecover(Throwable throwable, String name) {
669+
return 2;
670+
}
671+
672+
}
673+
642674
protected interface RecoverByRetryableNameInterface {
643675

644676
@Retryable(recover = "barRecover")
@@ -682,4 +714,28 @@ protected interface RecoverByRetryableNameWithPrimitiveArgsInterface {
682714

683715
}
684716

717+
protected interface RecoverByComposedRetryableAnnotationNameInterface {
718+
719+
@ComposedRetryable(recover = "barRecover")
720+
public int foo(String name);
721+
722+
@Recover
723+
public int fooRecover(Throwable throwable, String name);
724+
725+
@Recover
726+
public int barRecover(Throwable throwable, String name);
727+
728+
}
729+
730+
@Target({ ElementType.METHOD, ElementType.TYPE })
731+
@Retention(RetentionPolicy.RUNTIME)
732+
@Documented
733+
@Retryable(maxAttempts = 4)
734+
public @interface ComposedRetryable {
735+
736+
@AliasFor(annotation = Retryable.class, attribute = "recover")
737+
String recover() default "";
738+
739+
}
740+
685741
}

0 commit comments

Comments
 (0)