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
This commit is contained in:
88
README.md
88
README.md
@@ -595,6 +595,94 @@ line to your `build.gradle` file:
|
||||
```
|
||||
runtime('org.aspectj:aspectjweaver:1.8.13')
|
||||
```
|
||||
### Further customizations
|
||||
|
||||
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.
|
||||
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
|
||||
to create two custom annotations `@LocalRetryable` and `@RemoteRetryable` that differs in the retry strategy as well in the maximum number of retries.
|
||||
|
||||
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
|
||||
to be picked up as if it was set on the `recover` method of the base `@Retryable` annotation.
|
||||
|
||||
Usage Example:
|
||||
```java
|
||||
@Service
|
||||
class Service {
|
||||
...
|
||||
|
||||
@LocalRetryable(include = TemporaryLocalException.class, recover = "service1Recovery")
|
||||
public List<Thing> service1(String str1, String str2){
|
||||
//... do something
|
||||
}
|
||||
|
||||
public List<Thing> service1Recovery(TemporaryLocalException ex,String str1, String str2){
|
||||
//... Error handling for service1
|
||||
}
|
||||
...
|
||||
|
||||
@RemoteRetryable(include = TemporaryRemoteException.class, recover = "service2Recovery")
|
||||
public List<Thing> service2(String str1, String str2){
|
||||
//... do something
|
||||
}
|
||||
|
||||
public List<Thing> service2Recovery(TemporaryRemoteException ex, String str1, String str2){
|
||||
//... Error handling for service2
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Retryable(maxAttempts = "3", backoff = @Backoff(delay = "500", maxDelay = "2000", random = true)
|
||||
)
|
||||
public @interface LocalRetryable {
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "recover")
|
||||
String recover() default "";
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "value")
|
||||
Class<? extends Throwable>[] value() default {};
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "include")
|
||||
|
||||
Class<? extends Throwable>[] include() default {};
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "exclude")
|
||||
Class<? extends Throwable>[] exclude() default {};
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "label")
|
||||
String label() default "";
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Retryable(maxAttempts = "5", backoff = @Backoff(delay = "1000", maxDelay = "30000", multiplier = "1.2", random = true)
|
||||
)
|
||||
public @interface RemoteRetryable {
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "recover")
|
||||
String recover() default "";
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "value")
|
||||
Class<? extends Throwable>[] value() default {};
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "include")
|
||||
Class<? extends Throwable>[] include() default {};
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "exclude")
|
||||
Class<? extends Throwable>[] exclude() default {};
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "label")
|
||||
String label() default "";
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### XML Configuration
|
||||
|
||||
|
||||
@@ -23,14 +23,13 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.classify.SubclassClassifier;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.retry.ExhaustedRetryException;
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.interceptor.MethodInvocationRecoverer;
|
||||
import org.springframework.retry.support.RetrySynchronizationManager;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.ReflectionUtils.MethodCallback;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -53,6 +52,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Maksim Kita
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
* @author Gianluca Medici
|
||||
*/
|
||||
public class RecoverAnnotationRecoveryHandler<T> implements MethodInvocationRecoverer<T> {
|
||||
|
||||
@@ -199,14 +199,14 @@ public class RecoverAnnotationRecoveryHandler<T> implements MethodInvocationReco
|
||||
private void init(final Object target, Method method) {
|
||||
final Map<Class<? extends Throwable>, Method> types = new HashMap<Class<? extends Throwable>, Method>();
|
||||
final Method failingMethod = method;
|
||||
Retryable retryable = AnnotationUtils.findAnnotation(method, Retryable.class);
|
||||
Retryable retryable = AnnotatedElementUtils.findMergedAnnotation(method, Retryable.class);
|
||||
if (retryable != null) {
|
||||
this.recoverMethodName = retryable.recover();
|
||||
}
|
||||
ReflectionUtils.doWithMethods(target.getClass(), new MethodCallback() {
|
||||
@Override
|
||||
public void doWith(Method method) throws IllegalArgumentException {
|
||||
Recover recover = AnnotationUtils.findAnnotation(method, Recover.class);
|
||||
Recover recover = AnnotatedElementUtils.findMergedAnnotation(method, Recover.class);
|
||||
if (recover == null) {
|
||||
recover = findAnnotationOnTarget(target, method);
|
||||
}
|
||||
@@ -276,7 +276,7 @@ public class RecoverAnnotationRecoveryHandler<T> implements MethodInvocationReco
|
||||
private Recover findAnnotationOnTarget(Object target, Method method) {
|
||||
try {
|
||||
Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
|
||||
return AnnotationUtils.findAnnotation(targetMethod, Recover.class);
|
||||
return AnnotatedElementUtils.findMergedAnnotation(targetMethod, Recover.class);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
package org.springframework.retry.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -26,6 +31,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.retry.ExhaustedRetryException;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
@@ -40,6 +46,7 @@ import static org.junit.Assert.assertNotNull;
|
||||
* @author Randell Callahan
|
||||
* @author Nathanaël Roberts
|
||||
* @author Maksim Kita
|
||||
* @Author Gianluca Medici
|
||||
*/
|
||||
public class RecoverAnnotationRecoveryHandlerTests {
|
||||
|
||||
@@ -283,6 +290,14 @@ public class RecoverAnnotationRecoveryHandlerTests {
|
||||
assertEquals(2, handler.recover(new Object[] { 2 }, new RuntimeException("Planned")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recoverByComposedRetryableAnnotationName() {
|
||||
Method foo = ReflectionUtils.findMethod(RecoverByComposedRetryableAnnotationName.class, "foo", String.class);
|
||||
RecoverAnnotationRecoveryHandler<?> handler = new RecoverAnnotationRecoveryHandler<Integer>(
|
||||
new RecoverByComposedRetryableAnnotationName(), foo);
|
||||
assertThat(handler.recover(new Object[] { "Kevin" }, new RuntimeException("Planned"))).isEqualTo(4);
|
||||
}
|
||||
|
||||
private static class InAccessibleRecover {
|
||||
|
||||
@Retryable
|
||||
@@ -639,6 +654,23 @@ public class RecoverAnnotationRecoveryHandlerTests {
|
||||
|
||||
}
|
||||
|
||||
protected static class RecoverByComposedRetryableAnnotationName
|
||||
implements RecoverByComposedRetryableAnnotationNameInterface {
|
||||
|
||||
public int foo(String name) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int fooRecover(Throwable throwable, String name) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int barRecover(Throwable throwable, String name) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected interface RecoverByRetryableNameInterface {
|
||||
|
||||
@Retryable(recover = "barRecover")
|
||||
@@ -682,4 +714,28 @@ public class RecoverAnnotationRecoveryHandlerTests {
|
||||
|
||||
}
|
||||
|
||||
protected interface RecoverByComposedRetryableAnnotationNameInterface {
|
||||
|
||||
@ComposedRetryable(recover = "barRecover")
|
||||
public int foo(String name);
|
||||
|
||||
@Recover
|
||||
public int fooRecover(Throwable throwable, String name);
|
||||
|
||||
@Recover
|
||||
public int barRecover(Throwable throwable, String name);
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Retryable(maxAttempts = 4)
|
||||
public @interface ComposedRetryable {
|
||||
|
||||
@AliasFor(annotation = Retryable.class, attribute = "recover")
|
||||
String recover() default "";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user