Add optional recovery callback for stateless retry interceptor
This commit is contained in:
@@ -1,24 +1,25 @@
|
||||
/*
|
||||
* Copyright 2006-2007 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.
|
||||
*
|
||||
* 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.interceptor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.aop.ProxyMethodInvocation;
|
||||
import org.springframework.retry.ExhaustedRetryException;
|
||||
import org.springframework.retry.RecoveryCallback;
|
||||
import org.springframework.retry.RetryCallback;
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.RetryOperations;
|
||||
@@ -26,17 +27,14 @@ import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link MethodInterceptor} that can be used to automatically retry calls to
|
||||
* a method on a service if it fails. The injected {@link RetryOperations} is
|
||||
* used to control the number of retries. By default it will retry a fixed
|
||||
* number of times, according to the defaults in {@link RetryTemplate}.<br/>
|
||||
* A {@link MethodInterceptor} that can be used to automatically retry calls to a method on a service if it fails. The
|
||||
* injected {@link RetryOperations} is used to control the number of retries. By default it will retry a fixed number of
|
||||
* times, according to the defaults in {@link RetryTemplate}.<br/>
|
||||
*
|
||||
* Hint about transaction boundaries. If you want to retry a failed transaction
|
||||
* you need to make sure that the transaction boundary is inside the retry,
|
||||
* otherwise the successful attempt will roll back with the whole transaction.
|
||||
* If the method being intercepted is also transactional, then use the ordering
|
||||
* hints in the advice declarations to ensure that this one is before the
|
||||
* transaction interceptor in the advice chain.
|
||||
* Hint about transaction boundaries. If you want to retry a failed transaction you need to make sure that the
|
||||
* transaction boundary is inside the retry, otherwise the successful attempt will roll back with the whole transaction.
|
||||
* If the method being intercepted is also transactional, then use the ordering hints in the advice declarations to
|
||||
* ensure that this one is before the transaction interceptor in the advice chain.
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Dave Syer
|
||||
@@ -44,31 +42,32 @@ import org.springframework.util.Assert;
|
||||
public class RetryOperationsInterceptor implements MethodInterceptor {
|
||||
|
||||
private RetryOperations retryOperations = new RetryTemplate();
|
||||
private MethodInvocationRecoverer<?> recoverer;
|
||||
|
||||
public void setRetryOperations(RetryOperations retryTemplate) {
|
||||
Assert.notNull(retryTemplate, "'retryOperations' cannot be null.");
|
||||
this.retryOperations = retryTemplate;
|
||||
}
|
||||
|
||||
public void setRecoverer(MethodInvocationRecoverer<?> recoverer) {
|
||||
this.recoverer = recoverer;
|
||||
}
|
||||
|
||||
public Object invoke(final MethodInvocation invocation) throws Throwable {
|
||||
|
||||
return this.retryOperations.execute(new RetryCallback<Object>() {
|
||||
RetryCallback<Object> retryCallback = new RetryCallback<Object>() {
|
||||
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
|
||||
/*
|
||||
* If we don't copy the invocation carefully it won't keep a
|
||||
* reference to the other interceptors in the chain. We don't
|
||||
* have a choice here but to specialise to
|
||||
* ReflectiveMethodInvocation (but how often would another
|
||||
* implementation come along?).
|
||||
* If we don't copy the invocation carefully it won't keep a reference to the other interceptors in the
|
||||
* chain. We don't have a choice here but to specialise to ReflectiveMethodInvocation (but how often
|
||||
* would another implementation come along?).
|
||||
*/
|
||||
if (invocation instanceof ProxyMethodInvocation) {
|
||||
try {
|
||||
return ((ProxyMethodInvocation) invocation)
|
||||
.invocableClone().proceed();
|
||||
}
|
||||
catch (Exception e) {
|
||||
return ((ProxyMethodInvocation) invocation).invocableClone().proceed();
|
||||
} catch (Exception e) {
|
||||
throw e;
|
||||
} catch (Error e) {
|
||||
throw e;
|
||||
@@ -81,6 +80,42 @@ public class RetryOperationsInterceptor implements MethodInterceptor {
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
if (recoverer != null) {
|
||||
ItemRecovererCallback recoveryCallback = new ItemRecovererCallback(invocation.getArguments(), recoverer);
|
||||
return this.retryOperations.execute(retryCallback, recoveryCallback);
|
||||
}
|
||||
|
||||
return this.retryOperations.execute(retryCallback);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
private static final class ItemRecovererCallback implements RecoveryCallback<Object> {
|
||||
|
||||
private final Object[] args;
|
||||
|
||||
private final MethodInvocationRecoverer<? extends Object> recoverer;
|
||||
|
||||
/**
|
||||
* @param args the item that failed.
|
||||
*/
|
||||
private ItemRecovererCallback(Object[] args, MethodInvocationRecoverer<? extends Object> recoverer) {
|
||||
this.args = Arrays.asList(args).toArray();
|
||||
this.recoverer = recoverer;
|
||||
}
|
||||
|
||||
public Object recover(RetryContext context) {
|
||||
if (recoverer != null) {
|
||||
return recoverer.recover(args, context.getLastThrowable());
|
||||
}
|
||||
throw new ExhaustedRetryException("Retry was exhausted but there was no recovery path.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,16 +16,21 @@
|
||||
|
||||
package org.springframework.retry.interceptor;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.lang.reflect.AccessibleObject;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.target.SingletonTargetSource;
|
||||
@@ -37,7 +42,7 @@ import org.springframework.transaction.support.TransactionSynchronizationAdapter
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
public class RetryOperationsInterceptorTests extends TestCase {
|
||||
public class RetryOperationsInterceptorTests {
|
||||
|
||||
private RetryOperationsInterceptor interceptor;
|
||||
|
||||
@@ -49,8 +54,8 @@ public class RetryOperationsInterceptorTests extends TestCase {
|
||||
|
||||
private static int transactionCount;
|
||||
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
interceptor = new RetryOperationsInterceptor();
|
||||
target = new ServiceImpl();
|
||||
service = (Service) ProxyFactory.getProxy(Service.class, new SingletonTargetSource(target));
|
||||
@@ -58,12 +63,30 @@ public class RetryOperationsInterceptorTests extends TestCase {
|
||||
transactionCount = 0;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultInterceptorSunnyDay() throws Exception {
|
||||
((Advised) service).addAdvice(interceptor);
|
||||
service.service();
|
||||
assertEquals(2, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultInterceptorWithRecovery() throws Exception {
|
||||
RetryTemplate template = new RetryTemplate();
|
||||
template.setRetryPolicy(new SimpleRetryPolicy(1, Collections
|
||||
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true)));
|
||||
interceptor.setRetryOperations(template);
|
||||
interceptor.setRecoverer(new MethodInvocationRecoverer<Void>() {
|
||||
public Void recover(Object[] args, Throwable cause) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
((Advised) service).addAdvice(interceptor);
|
||||
service.service();
|
||||
assertEquals(1, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterceptorChainWithRetry() throws Exception {
|
||||
((Advised) service).addAdvice(interceptor);
|
||||
final List<String> list = new ArrayList<String>();
|
||||
@@ -82,6 +105,7 @@ public class RetryOperationsInterceptorTests extends TestCase {
|
||||
assertEquals(2, list.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetryExceptionAfterTooManyAttempts() throws Exception {
|
||||
((Advised) service).addAdvice(interceptor);
|
||||
RetryTemplate template = new RetryTemplate();
|
||||
@@ -97,6 +121,7 @@ public class RetryOperationsInterceptorTests extends TestCase {
|
||||
assertEquals(1, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutsideTransaction() throws Exception {
|
||||
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(ClassUtils
|
||||
.addResourcePathToPackagePath(getClass(), "retry-transaction-test.xml"));
|
||||
@@ -110,6 +135,7 @@ public class RetryOperationsInterceptorTests extends TestCase {
|
||||
assertEquals(2, transactionCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIllegalMethodInvocationType() throws Throwable {
|
||||
try {
|
||||
interceptor.invoke(new MethodInvocation() {
|
||||
|
||||
Reference in New Issue
Block a user