From 9fb92bccbd3c26b9a01d08b01abb313d77a7f526 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 24 Apr 2014 09:41:26 +0100 Subject: [PATCH] Add support for @Recover --- README.md | 8 +- .../classify/BackToBackPatternClassifier.java | 2 +- ...tationAwareRetryOperationsInterceptor.java | 42 ++++- .../retry/{config => annotation}/Backoff.java | 2 +- .../{config => annotation}/EnableRetry.java | 2 +- .../retry/annotation/Recover.java | 44 +++++ .../RecoverAnnotationRecoveryHandler.java | 162 ++++++++++++++++++ .../RetryConfiguration.java | 2 +- .../{config => annotation}/Retryable.java | 2 +- .../EnableRetryTests.java | 48 +++++- .../EnableRetryWithBackoffTests.java | 5 +- ...RecoverAnnotationRecoveryHandlerTests.java | 157 +++++++++++++++++ 12 files changed, 459 insertions(+), 17 deletions(-) rename src/main/java/org/springframework/retry/{config => annotation}/AnnotationAwareRetryOperationsInterceptor.java (80%) rename src/main/java/org/springframework/retry/{config => annotation}/Backoff.java (98%) rename src/main/java/org/springframework/retry/{config => annotation}/EnableRetry.java (96%) create mode 100644 src/main/java/org/springframework/retry/annotation/Recover.java create mode 100644 src/main/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandler.java rename src/main/java/org/springframework/retry/{config => annotation}/RetryConfiguration.java (98%) rename src/main/java/org/springframework/retry/{config => annotation}/Retryable.java (98%) rename src/test/java/org/springframework/retry/{config => annotation}/EnableRetryTests.java (79%) rename src/test/java/org/springframework/retry/{config => annotation}/EnableRetryWithBackoffTests.java (95%) create mode 100644 src/test/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandlerTests.java diff --git a/README.md b/README.md index f9bb621..59e8802 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,17 @@ public class Application { @Service class Service { @Retryable(RemoteAccessException.class) - public service() { + public void service() { // ... do something } + @Recover + public void recover(RemoteAccessException e) { + // ... panic + } } ``` -Call the "service" method and if it fails with a `RemoteAccessException` then it will retry (up to three times by default). There are various options in the `@Retryable` annotation attributes for including and excluding exception types, limiting the number of retries and the policy for backoff. +Call the "service" method and if it fails with a `RemoteAccessException` then it will retry (up to three times by default), and then execute the "recover" method if unsuccessful. There are various options in the `@Retryable` annotation attributes for including and excluding exception types, limiting the number of retries and the policy for backoff. ## Building diff --git a/src/main/java/org/springframework/classify/BackToBackPatternClassifier.java b/src/main/java/org/springframework/classify/BackToBackPatternClassifier.java index a3ec528..0559409 100644 --- a/src/main/java/org/springframework/classify/BackToBackPatternClassifier.java +++ b/src/main/java/org/springframework/classify/BackToBackPatternClassifier.java @@ -62,7 +62,7 @@ public class BackToBackPatternClassifier implements Classifier { /** * A convenience method of creating a router classifier based on a plain old * Java Object. The object provided must have precisely one public method - * that either has the @Classifier annotation or accepts a single argument + * that either has the @Classifier annotation or accepts a single argument * and outputs a String. This will be used to create an input classifier for * the router component.
* diff --git a/src/main/java/org/springframework/retry/config/AnnotationAwareRetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java similarity index 80% rename from src/main/java/org/springframework/retry/config/AnnotationAwareRetryOperationsInterceptor.java rename to src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java index dde4030..9c9da8d 100644 --- a/src/main/java/org/springframework/retry/config/AnnotationAwareRetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package org.springframework.retry.config; +package org.springframework.retry.annotation; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -28,9 +29,10 @@ import org.springframework.retry.backoff.BackOffPolicy; import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.backoff.ExponentialRandomBackOffPolicy; import org.springframework.retry.backoff.FixedBackOffPolicy; -import org.springframework.retry.backoff.UniformRandomBackOffPolicy; import org.springframework.retry.backoff.Sleeper; +import org.springframework.retry.backoff.UniformRandomBackOffPolicy; import org.springframework.retry.interceptor.MethodArgumentsKeyGenerator; +import org.springframework.retry.interceptor.MethodInvocationRecoverer; import org.springframework.retry.interceptor.NewMethodArgumentsIdentifier; import org.springframework.retry.interceptor.RetryOperationsInterceptor; import org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor; @@ -38,6 +40,8 @@ import org.springframework.retry.policy.MapRetryContextCache; import org.springframework.retry.policy.RetryContextCache; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodCallback; /** * Wrapper interceptor that interprets the retry metadata on the method it is invoking and @@ -91,11 +95,11 @@ public class AnnotationAwareRetryOperationsInterceptor implements MethodIntercep } public Object invoke(MethodInvocation invocation) throws Throwable { - MethodInterceptor delegate = getDelegate(invocation.getMethod()); + MethodInterceptor delegate = getDelegate(invocation.getThis(), invocation.getMethod()); return delegate.invoke(invocation); } - private MethodInterceptor getDelegate(Method method) { + private MethodInterceptor getDelegate(Object target, Method method) { if (!delegates.containsKey(method)) { synchronized (delegates) { if (!delegates.containsKey(method)) { @@ -107,9 +111,9 @@ public class AnnotationAwareRetryOperationsInterceptor implements MethodIntercep } MethodInterceptor delegate; if (retryable.stateful()) { - delegate = getStatefulInterceptor(retryable); + delegate = getStatefulInterceptor(target, method, retryable); } else { - delegate = getStatelessInterceptor(retryable); + delegate = getStatelessInterceptor(target, method, retryable); } // TODO: allow declaring class to specify @Recover methods delegates.put(method, delegate); @@ -119,16 +123,17 @@ public class AnnotationAwareRetryOperationsInterceptor implements MethodIntercep return delegates.get(method); } - private MethodInterceptor getStatelessInterceptor(Retryable retryable) { + private MethodInterceptor getStatelessInterceptor(Object target, Method method, Retryable retryable) { RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor(); RetryTemplate template = new RetryTemplate(); template.setRetryPolicy(getRetryPolicy(retryable)); template.setBackOffPolicy(getBackoffPolicy(retryable.backoff())); interceptor.setRetryOperations(template); + interceptor.setRecoverer(getRecoverer(target, method)); return interceptor; } - private MethodInterceptor getStatefulInterceptor(Retryable retryable) { + private MethodInterceptor getStatefulInterceptor(Object target, Method method, Retryable retryable) { StatefulRetryOperationsInterceptor interceptor = new StatefulRetryOperationsInterceptor(); if (methodArgumentsKeyGenerator != null) { interceptor.setKeyGenerator(methodArgumentsKeyGenerator); @@ -141,9 +146,30 @@ public class AnnotationAwareRetryOperationsInterceptor implements MethodIntercep template.setRetryPolicy(getRetryPolicy(retryable)); template.setBackOffPolicy(getBackoffPolicy(retryable.backoff())); interceptor.setRetryOperations(template); + interceptor.setRecoverer(getRecoverer(target, method)); return interceptor; } + private MethodInvocationRecoverer getRecoverer(Object target, Method method) { + if (target instanceof MethodInvocationRecoverer) { + return (MethodInvocationRecoverer) target; + } + final AtomicBoolean foundRecoverable = new AtomicBoolean(false); + ReflectionUtils.doWithMethods(target.getClass(), new MethodCallback() { + @Override + public void doWith(Method method) throws IllegalArgumentException, + IllegalAccessException { + if (AnnotationUtils.findAnnotation(method, Recover.class)!=null) { + foundRecoverable.set(true); + } + } + }); + if (!foundRecoverable.get()) { + return null; + } + return new RecoverAnnotationRecoveryHandler(target, method); + } + private RetryPolicy getRetryPolicy(Retryable retryable) { Class[] includes = retryable.value(); if (includes.length == 0) { diff --git a/src/main/java/org/springframework/retry/config/Backoff.java b/src/main/java/org/springframework/retry/annotation/Backoff.java similarity index 98% rename from src/main/java/org/springframework/retry/config/Backoff.java rename to src/main/java/org/springframework/retry/annotation/Backoff.java index 051cd05..38ea11c 100644 --- a/src/main/java/org/springframework/retry/config/Backoff.java +++ b/src/main/java/org/springframework/retry/annotation/Backoff.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.retry.config; +package org.springframework.retry.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/src/main/java/org/springframework/retry/config/EnableRetry.java b/src/main/java/org/springframework/retry/annotation/EnableRetry.java similarity index 96% rename from src/main/java/org/springframework/retry/config/EnableRetry.java rename to src/main/java/org/springframework/retry/annotation/EnableRetry.java index c504b96..77ebd6a 100644 --- a/src/main/java/org/springframework/retry/config/EnableRetry.java +++ b/src/main/java/org/springframework/retry/annotation/EnableRetry.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.retry.config; +package org.springframework.retry.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/src/main/java/org/springframework/retry/annotation/Recover.java b/src/main/java/org/springframework/retry/annotation/Recover.java new file mode 100644 index 0000000..9ca936a --- /dev/null +++ b/src/main/java/org/springframework/retry/annotation/Recover.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 org.springframework.context.annotation.Import; + +/** + * Annotation for a method invocation that is a recovery handler. A suitable recovery + * handler has a first parameter of type Throwable (or a subtype of Throwable) and a + * return value of the same type as the @Retryable method to recover from. + * The Throwable first argument is optional (but a method without it will only be called + * if no others match). Subsequent arguments are populated from the argument list of the + * failed method in order. + * + * @author Dave Syer + * @since 2.0 + * + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Import(RetryConfiguration.class) +@Documented +public @interface Recover { +} diff --git a/src/main/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandler.java b/src/main/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandler.java new file mode 100644 index 0000000..c790380 --- /dev/null +++ b/src/main/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandler.java @@ -0,0 +1,162 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.annotation; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.classify.SubclassClassifier; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.retry.ExhaustedRetryException; +import org.springframework.retry.interceptor.MethodInvocationRecoverer; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodCallback; + +/** + * A recoverer for method invocations based on the @Recover annotation. A + * suitable recovery method is one with a Throwable type as the first parameter and the + * same return type and arguments as the method that failed. The Throwable first argument + * is optional and if omitted the method is treated as a default (called when there are no + * other matches). Generally the best matching method is chosen based on the type of the + * first parameter and the type of the exception being handled. The closest match in the + * class hierarchy is chosen, so for instance if an IllegalArgumentException is being + * handled and there is a method whose first argument is RuntimeException, then it will be + * preferred over a method whose first argument is Throwable. + * + * @author Dave Syer + * + */ +public class RecoverAnnotationRecoveryHandler implements MethodInvocationRecoverer { + + private SubclassClassifier classifier = new SubclassClassifier(); + private Map methods = new HashMap(); + private Object target; + + public RecoverAnnotationRecoveryHandler(Object target, Method method) { + this.target = target; + init(target, method); + } + + @Override + public T recover(Object[] args, Throwable cause) { + Method method = findClosestMatch(cause.getClass()); + if (method == null) { + throw new ExhaustedRetryException("Cannot locate recovery method", cause); + } + SimpleMetadata meta = methods.get(method); + Object[] argsToUse = meta.getArgs(cause, args); + @SuppressWarnings("unchecked") + T result = (T) ReflectionUtils.invokeMethod(method, target, argsToUse); + return result; + } + + private Method findClosestMatch(Class cause) { + int min = Integer.MAX_VALUE; + Method result = null; + for (Method method : methods.keySet()) { + SimpleMetadata meta = methods.get(method); + Class type = meta.getType(); + if (type == null) { + type = Throwable.class; + } + if (type.isAssignableFrom(cause)) { + int distance = calculateDistance(cause, type); + if (distance < min) { + min = distance; + result = method; + } + } + } + return result; + } + + private int calculateDistance(Class cause, + Class type) { + int result = 0; + Class current = cause; + while (current != type && current != Throwable.class) { + result++; + current = current.getSuperclass(); + } + return result; + } + + private void init(Object target, Method method) { + final Map, Method> types = new HashMap, Method>(); + final Method failingMethod = method; + ReflectionUtils.doWithMethods(failingMethod.getDeclaringClass(), + new MethodCallback() { + @Override + public void doWith(Method method) throws IllegalArgumentException, + IllegalAccessException { + Recover recover = AnnotationUtils.findAnnotation(method, + Recover.class); + if (recover != null + && failingMethod.getReturnType().isAssignableFrom( + method.getReturnType())) { + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length > 0 + && Throwable.class + .isAssignableFrom(parameterTypes[0])) { + @SuppressWarnings("unchecked") + Class type = (Class) parameterTypes[0]; + types.put(type, method); + methods.put(method, new SimpleMetadata( + parameterTypes.length, type)); + } else { + classifier.setDefaultValue(method); + methods.put(method, new SimpleMetadata( + parameterTypes.length, null)); + } + } + } + }); + classifier.setTypeMap(types); + } + + private static class SimpleMetadata { + private int argCount; + private Class type; + + public SimpleMetadata(int argCount, Class type) { + super(); + this.argCount = argCount; + this.type = type; + } + + public int getArgCount() { + return argCount; + } + + public Class getType() { + return type; + } + + public Object[] getArgs(Throwable t, Object[] args) { + Object[] result = new Object[getArgCount()]; + int startArgs = 0; + if (type != null) { + result[0] = t; + startArgs = 1; + } + System.arraycopy(args, 0, result, startArgs, result.length - startArgs); + return result; + } + } + +} diff --git a/src/main/java/org/springframework/retry/config/RetryConfiguration.java b/src/main/java/org/springframework/retry/annotation/RetryConfiguration.java similarity index 98% rename from src/main/java/org/springframework/retry/config/RetryConfiguration.java rename to src/main/java/org/springframework/retry/annotation/RetryConfiguration.java index eb9dd1b..4faa19e 100644 --- a/src/main/java/org/springframework/retry/config/RetryConfiguration.java +++ b/src/main/java/org/springframework/retry/annotation/RetryConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.retry.config; +package org.springframework.retry.annotation; import java.lang.annotation.Annotation; import java.util.LinkedHashSet; diff --git a/src/main/java/org/springframework/retry/config/Retryable.java b/src/main/java/org/springframework/retry/annotation/Retryable.java similarity index 98% rename from src/main/java/org/springframework/retry/config/Retryable.java rename to src/main/java/org/springframework/retry/annotation/Retryable.java index dca1302..01f570a 100644 --- a/src/main/java/org/springframework/retry/config/Retryable.java +++ b/src/main/java/org/springframework/retry/annotation/Retryable.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.retry.config; +package org.springframework.retry.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/src/test/java/org/springframework/retry/config/EnableRetryTests.java b/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java similarity index 79% rename from src/test/java/org/springframework/retry/config/EnableRetryTests.java rename to src/test/java/org/springframework/retry/annotation/EnableRetryTests.java index b3b625a..851adb5 100644 --- a/src/test/java/org/springframework/retry/config/EnableRetryTests.java +++ b/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package org.springframework.retry.config; +package org.springframework.retry.annotation; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import org.junit.Test; @@ -24,6 +25,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.retry.annotation.EnableRetry; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; import org.springframework.retry.backoff.Sleeper; /** @@ -42,6 +46,17 @@ public class EnableRetryTests { context.close(); } + @Test + public void recovery() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfiguration.class); + RecoverableService service = context.getBean(RecoverableService.class); + service.service(); + assertEquals(3, service.getCount()); + assertNotNull(service.getCause()); + context.close(); + } + @Test public void type() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( @@ -101,6 +116,11 @@ public class EnableRetryTests { return new Service(); } + @Bean + public RecoverableService recoverable() { + return new RecoverableService(); + } + @Bean public RetryableService retryable() { return new RetryableService(); @@ -135,6 +155,32 @@ public class EnableRetryTests { } + protected static class RecoverableService { + + private int count = 0; + private Throwable cause; + + @Retryable(RuntimeException.class) + public void service() { + count++; + throw new RuntimeException("Planned"); + } + + @Recover + public void recover(Throwable cause) { + this.cause = cause; + } + + public Throwable getCause() { + return cause; + } + + public int getCount() { + return count; + } + + } + @Retryable(RuntimeException.class) protected static class RetryableService { diff --git a/src/test/java/org/springframework/retry/config/EnableRetryWithBackoffTests.java b/src/test/java/org/springframework/retry/annotation/EnableRetryWithBackoffTests.java similarity index 95% rename from src/test/java/org/springframework/retry/config/EnableRetryWithBackoffTests.java rename to src/test/java/org/springframework/retry/annotation/EnableRetryWithBackoffTests.java index 88c889c..0f19b82 100644 --- a/src/test/java/org/springframework/retry/config/EnableRetryWithBackoffTests.java +++ b/src/test/java/org/springframework/retry/annotation/EnableRetryWithBackoffTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.retry.config; +package org.springframework.retry.annotation; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -28,6 +28,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.EnableRetry; +import org.springframework.retry.annotation.Retryable; import org.springframework.retry.backoff.Sleeper; /** diff --git a/src/test/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandlerTests.java b/src/test/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandlerTests.java new file mode 100644 index 0000000..d461224 --- /dev/null +++ b/src/test/java/org/springframework/retry/annotation/RecoverAnnotationRecoveryHandlerTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.annotation; + +import static org.junit.Assert.assertEquals; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.retry.ExhaustedRetryException; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler; +import org.springframework.retry.annotation.Retryable; +import org.springframework.util.ReflectionUtils; + +/** + * @author Dave Syer + * + */ +public class RecoverAnnotationRecoveryHandlerTests { + + @Rule + public ExpectedException expected = ExpectedException.none(); + + @Test + public void defaultRecoverMethod() { + RecoverAnnotationRecoveryHandler handler = new RecoverAnnotationRecoveryHandler( + new DefaultRecover(), ReflectionUtils.findMethod(DefaultRecover.class, + "foo", String.class)); + assertEquals(1, + handler.recover(new Object[] { "Dave" }, new RuntimeException("Planned"))); + } + + @Test + public void fewerArgs() { + RecoverAnnotationRecoveryHandler handler = new RecoverAnnotationRecoveryHandler( + new FewerArgs(), ReflectionUtils.findMethod(FewerArgs.class, "foo", + String.class, int.class)); + assertEquals(1, + handler.recover(new Object[] { "Dave" }, new RuntimeException("Planned"))); + } + + @Test + public void noArgs() { + NoArgs target = new NoArgs(); + RecoverAnnotationRecoveryHandler handler = new RecoverAnnotationRecoveryHandler( + target, ReflectionUtils.findMethod(NoArgs.class, "foo")); + handler.recover(new Object[0], new RuntimeException("Planned")); + assertEquals("Planned", target.getCause().getMessage()); + } + + @Test + public void noMatch() { + RecoverAnnotationRecoveryHandler handler = new RecoverAnnotationRecoveryHandler( + new SpecificException(), ReflectionUtils.findMethod( + SpecificException.class, "foo", String.class)); + expected.expect(ExhaustedRetryException.class); + handler.recover(new Object[] { "Dave" }, new Error("Planned")); + } + + @Test + public void specificRecoverMethod() { + RecoverAnnotationRecoveryHandler handler = new RecoverAnnotationRecoveryHandler( + new SpecificRecover(), ReflectionUtils.findMethod(SpecificRecover.class, + "foo", String.class)); + assertEquals(2, + handler.recover(new Object[] { "Dave" }, new RuntimeException("Planned"))); + } + + protected static class DefaultRecover { + @Retryable + public int foo(String name) { + return 0; + } + + @Recover + public int bar(String name) { + return 1; + } + } + + protected static class NoArgs { + private Throwable cause; + + @Retryable + public void foo() { + } + + @Recover + public void bar(Throwable cause) { + this.cause = cause; + } + + public Throwable getCause() { + return cause; + } + } + + protected static class SpecificRecover { + @Retryable + public int foo(String name) { + return 0; + } + + @Recover + public int bar(String name) { + return 1; + } + + @Recover + public int bar(RuntimeException e, String name) { + return 2; + } + + } + + protected static class FewerArgs { + @Retryable + public int foo(String name, int value) { + return 0; + } + + @Recover + public int bar(RuntimeException e, String name) { + return 1; + } + + } + + protected static class SpecificException { + @Retryable + public int foo(String name) { + return 0; + } + + @Recover + public int bar(RuntimeException e, String name) { + return 1; + } + + } + +}