diff --git a/README.md b/README.md index 730721a..57374ae 100644 --- a/README.md +++ b/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 service1(String str1, String str2){ + //... do something + } + + public List service1Recovery(TemporaryLocalException ex,String str1, String str2){ + //... Error handling for service1 + } + ... + + @RemoteRetryable(include = TemporaryRemoteException.class, recover = "service2Recovery") + public List service2(String str1, String str2){ + //... do something + } + + public List 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[] value() default {}; + + @AliasFor(annotation = Retryable.class, attribute = "include") + + Class[] include() default {}; + + @AliasFor(annotation = Retryable.class, attribute = "exclude") + Class[] 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[] value() default {}; + + @AliasFor(annotation = Retryable.class, attribute = "include") + Class[] include() default {}; + + @AliasFor(annotation = Retryable.class, attribute = "exclude") + Class[] exclude() default {}; + + @AliasFor(annotation = Retryable.class, attribute = "label") + String label() default ""; + +} +``` ### XML Configuration diff --git a/src/main/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandler.java b/src/main/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandler.java index dcbc7be..64effdf 100644 --- a/src/main/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandler.java +++ b/src/main/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandler.java @@ -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 implements MethodInvocationRecoverer { @@ -199,14 +199,14 @@ public class RecoverAnnotationRecoveryHandler implements MethodInvocationReco private void init(final Object target, Method method) { final Map, Method> types = new HashMap, 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 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; diff --git a/src/test/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandlerTests.java b/src/test/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandlerTests.java index ee4cf28..e034155 100644 --- a/src/test/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandlerTests.java +++ b/src/test/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandlerTests.java @@ -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( + 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 ""; + + } + }