Add support for @Recover

This commit is contained in:
Dave Syer
2014-04-24 09:41:26 +01:00
parent 46c3ebde4e
commit 9fb92bccbd
12 changed files with 459 additions and 17 deletions

View File

@@ -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

View File

@@ -62,7 +62,7 @@ public class BackToBackPatternClassifier<C, T> implements Classifier<C, T> {
/**
* 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 <code>@Classifier</code> annotation or accepts a single argument
* and outputs a String. This will be used to create an input classifier for
* the router component. <br/>
*

View File

@@ -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<Object>(target, method);
}
private RetryPolicy getRetryPolicy(Retryable retryable) {
Class<? extends Throwable>[] includes = retryable.value();
if (includes.length == 0) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 <code>@Retryable</code> 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 {
}

View File

@@ -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 <code>@Recover</code> 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<T> implements MethodInvocationRecoverer<T> {
private SubclassClassifier<Throwable, Method> classifier = new SubclassClassifier<Throwable, Method>();
private Map<Method, SimpleMetadata> methods = new HashMap<Method, SimpleMetadata>();
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<? extends Throwable> cause) {
int min = Integer.MAX_VALUE;
Method result = null;
for (Method method : methods.keySet()) {
SimpleMetadata meta = methods.get(method);
Class<? extends Throwable> 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<? extends Throwable> cause,
Class<? extends Throwable> 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<Class<? extends Throwable>, Method> types = new HashMap<Class<? extends Throwable>, 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<? extends Throwable> type = (Class<? extends Throwable>) 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<? extends Throwable> type;
public SimpleMetadata(int argCount, Class<? extends Throwable> type) {
super();
this.argCount = argCount;
this.type = type;
}
public int getArgCount() {
return argCount;
}
public Class<? extends Throwable> 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;
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;
/**

View File

@@ -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<Integer>(
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<Integer>(
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<Integer>(
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<Integer>(
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<Integer>(
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;
}
}
}