Add RetryInterceptorBuilder support

* Add `RetryInterceptorBuilder` and its tests
* Add usage from `AnnotationAwareRetryOperationsInterceptor`
* Add `Retryable#interceptor()` option to use full Retry Interceptor from `BeanFactory`
* Fix `RetryConfiguration` `beanFactory` propagation

Fixes gh-11
This commit is contained in:
Artem Bilan
2014-05-12 14:22:23 +03:00
committed by Dave Syer
parent 1a54a3e1c2
commit f9e53099bd
7 changed files with 695 additions and 88 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2014 the original author or authors.
* Copyright 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.
@@ -23,7 +23,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.IntroductionInterceptor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.BackOffPolicy;
@@ -35,35 +39,38 @@ 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;
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
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;
import org.springframework.util.StringUtils;
/**
* WrappeMethodInterceptorr interceptor that interprets the retry metadata on the method it is invoking and
* delegates to an appropriate RetryOperationsInterceptor.
*
*
* @author Dave Syer
* @since 2.0
* @author Artem Bilan
* @since 1.1
*
*/
public class AnnotationAwareRetryOperationsInterceptor implements IntroductionInterceptor {
public class AnnotationAwareRetryOperationsInterceptor implements IntroductionInterceptor, BeanFactoryAware {
private Map<Method, MethodInterceptor> delegates = new HashMap<Method, MethodInterceptor>();
private final Map<Method, MethodInterceptor> delegates = new HashMap<Method, MethodInterceptor>();
private RetryContextCache retryContextCache = new MapRetryContextCache();
private MethodArgumentsKeyGenerator methodArgumentsKeyGenerator;
private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier;
private Sleeper sleeper;
private BeanFactory beanFactory;
/**
* @param sleeper the sleeper to set
*/
@@ -73,7 +80,7 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
/**
* Public setter for the {@link RetryContextCache}.
*
*
* @param retryContextCache the {@link RetryContextCache} to set.
*/
public void setRetryContextCache(RetryContextCache retryContextCache) {
@@ -81,78 +88,80 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
}
/**
* @param methodArgumentsKeyGenerator
* @param methodArgumentsKeyGenerator the {@link MethodArgumentsKeyGenerator}
*/
public void setKeyGenerator(MethodArgumentsKeyGenerator methodArgumentsKeyGenerator) {
this.methodArgumentsKeyGenerator = methodArgumentsKeyGenerator;
}
/**
* @param newMethodArgumentsIdentifier
* @param newMethodArgumentsIdentifier the {@link NewMethodArgumentsIdentifier}
*/
public void setNewItemIdentifier(
NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) {
public void setNewItemIdentifier(NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) {
this.newMethodArgumentsIdentifier = newMethodArgumentsIdentifier;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public boolean implementsInterface(Class<?> intf) {
return org.springframework.retry.interceptor.Retryable.class.isAssignableFrom(intf);
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
MethodInterceptor delegate = getDelegate(invocation.getThis(), invocation.getMethod());
return delegate.invoke(invocation);
}
@Override
public boolean implementsInterface(Class<?> intf) {
return Retryable.class.isAssignableFrom(intf);
}
private MethodInterceptor getDelegate(Object target, Method method) {
if (!delegates.containsKey(method)) {
synchronized (delegates) {
if (!delegates.containsKey(method)) {
Retryable retryable = AnnotationUtils.findAnnotation(method,
Retryable.class);
if (!this.delegates.containsKey(method)) {
synchronized (this.delegates) {
if (!this.delegates.containsKey(method)) {
Retryable retryable = AnnotationUtils.findAnnotation(method, Retryable.class);
if (retryable == null) {
retryable = AnnotationUtils.findAnnotation(
method.getDeclaringClass(), Retryable.class);
retryable = AnnotationUtils.findAnnotation(method.getDeclaringClass(), Retryable.class);
}
MethodInterceptor delegate;
if (retryable.stateful()) {
if (StringUtils.hasText(retryable.interceptor())) {
delegate = this.beanFactory.getBean(retryable.interceptor(), MethodInterceptor.class);
}
else if (retryable.stateful()) {
delegate = getStatefulInterceptor(target, method, retryable);
} else {
}
else {
delegate = getStatelessInterceptor(target, method, retryable);
}
delegates.put(method, delegate);
this.delegates.put(method, delegate);
}
}
}
return delegates.get(method);
return this.delegates.get(method);
}
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;
return RetryInterceptorBuilder.stateless()
.retryPolicy(getRetryPolicy(retryable))
.backOffPolicy(getBackoffPolicy(retryable.backoff()))
.recoverer(getRecoverer(target, method))
.build();
}
private MethodInterceptor getStatefulInterceptor(Object target, Method method, Retryable retryable) {
StatefulRetryOperationsInterceptor interceptor = new StatefulRetryOperationsInterceptor();
if (methodArgumentsKeyGenerator != null) {
interceptor.setKeyGenerator(methodArgumentsKeyGenerator);
}
if (newMethodArgumentsIdentifier != null) {
interceptor.setNewItemIdentifier(newMethodArgumentsIdentifier);
}
RetryTemplate template = new RetryTemplate();
template.setRetryContextCache(retryContextCache);
template.setRetryContextCache(this.retryContextCache);
template.setRetryPolicy(getRetryPolicy(retryable));
template.setBackOffPolicy(getBackoffPolicy(retryable.backoff()));
interceptor.setRetryOperations(template);
interceptor.setRecoverer(getRecoverer(target, method));
return interceptor;
return RetryInterceptorBuilder.stateful()
.retryOperations(template)
.recoverer(getRecoverer(target, method))
.keyGenerator(this.methodArgumentsKeyGenerator)
.newMethodArgumentsIdentifier(this.newMethodArgumentsIdentifier)
.build();
}
private MethodInvocationRecoverer<?> getRecoverer(Object target, Method method) {
@@ -164,11 +173,12 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
@Override
public void doWith(Method method) throws IllegalArgumentException,
IllegalAccessException {
if (AnnotationUtils.findAnnotation(method, Recover.class)!=null) {
if (AnnotationUtils.findAnnotation(method, Recover.class) != null) {
foundRecoverable.set(true);
}
}
});
if (!foundRecoverable.get()) {
return null;
}
@@ -193,39 +203,37 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
for (Class<? extends Throwable> type : excludes) {
policyMap.put(type, false);
}
SimpleRetryPolicy simple = new SimpleRetryPolicy(retryable.maxAttempts(),
policyMap, true);
return simple;
return new SimpleRetryPolicy(retryable.maxAttempts(), policyMap, true);
}
private BackOffPolicy getBackoffPolicy(Backoff backoff) {
long min = backoff.delay()==0 ? backoff.value() : backoff.delay();
long min = backoff.delay() == 0 ? backoff.value() : backoff.delay();
long max = backoff.maxDelay();
if (backoff.multiplier()>0) {
if (backoff.multiplier() > 0) {
ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy();
if (backoff.random()) {
policy = new ExponentialRandomBackOffPolicy();
}
policy.setInitialInterval(min);
policy.setMultiplier(backoff.multiplier());
policy.setMaxInterval(max>min ? max : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL);
if (sleeper!=null) {
policy.setMaxInterval(max > min ? max : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL);
if (sleeper != null) {
policy.setSleeper(sleeper);
}
return policy;
}
if (max>min) {
if (max > min) {
UniformRandomBackOffPolicy policy = new UniformRandomBackOffPolicy();
policy.setMinBackOffPeriod(min);
policy.setMaxBackOffPeriod(max);
if (sleeper!=null) {
if (sleeper != null) {
policy.setSleeper(sleeper);
}
return policy;
}
FixedBackOffPolicy policy = new FixedBackOffPolicy();
policy.setBackOffPeriod(min);
if (sleeper!=null) {
if (sleeper != null) {
policy.setSleeper(sleeper);
}
return policy;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 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.
@@ -23,6 +23,7 @@ import java.util.Set;
import javax.annotation.PostConstruct;
import org.aopalliance.aop.Advice;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.IntroductionAdvisor;
import org.springframework.aop.Pointcut;
@@ -44,9 +45,10 @@ import org.springframework.retry.policy.RetryContextCache;
* {@link RetryContextCache}, {@link MethodArgumentsKeyGenerator} or
* {@link NewMethodArgumentsIdentifier} it will be used by the corresponding
* retry interceptor (otherwise sensible defaults are adopted).
*
*
* @author Dave Syer
* @since 2.0
* @author Artem Bilan
* @since 1.1
*
*/
@SuppressWarnings("serial")
@@ -70,13 +72,17 @@ public class RetryConfiguration extends AbstractPointcutAdvisor implements
@Autowired(required = false)
private Sleeper sleeper;
private BeanFactory beanFactory;
@PostConstruct
public void init() {
Set<Class<? extends Annotation>> retryableAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(
1);
Set<Class<? extends Annotation>> retryableAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(1);
retryableAnnotationTypes.add(Retryable.class);
this.pointcut = buildPointcut(retryableAnnotationTypes);
this.advice = buildAdvice();
if (this.advice instanceof BeanFactoryAware) {
((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
}
}
/**
@@ -85,9 +91,7 @@ public class RetryConfiguration extends AbstractPointcutAdvisor implements
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) {
if (this.advice instanceof BeanFactoryAware) {
((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
}
this.beanFactory = beanFactory;
}
@Override
@@ -97,7 +101,7 @@ public class RetryConfiguration extends AbstractPointcutAdvisor implements
@Override
public Class<?>[] getInterfaces() {
return new Class[] { org.springframework.retry.interceptor.Retryable.class };
return new Class[] {org.springframework.retry.interceptor.Retryable.class};
}
@Override
@@ -133,22 +137,20 @@ public class RetryConfiguration extends AbstractPointcutAdvisor implements
/**
* Calculate a pointcut for the given retry annotation types, if any.
*
*
* @param retryAnnotationTypes
* the retry annotation types to introspect
* @return the applicable Pointcut object, or {@code null} if none
*/
protected Pointcut buildPointcut(
Set<Class<? extends Annotation>> retryAnnotationTypes) {
protected Pointcut buildPointcut(Set<Class<? extends Annotation>> retryAnnotationTypes) {
ComposablePointcut result = null;
for (Class<? extends Annotation> retryAnnotationType : retryAnnotationTypes) {
Pointcut cpc = new AnnotationMatchingPointcut(retryAnnotationType,
true);
Pointcut mpc = AnnotationMatchingPointcut
.forMethodAnnotation(retryAnnotationType);
Pointcut cpc = new AnnotationMatchingPointcut(retryAnnotationType, true);
Pointcut mpc = AnnotationMatchingPointcut.forMethodAnnotation(retryAnnotationType);
if (result == null) {
result = new ComposablePointcut(cpc).union(mpc);
} else {
}
else {
result.union(cpc).union(mpc);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 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.
@@ -24,9 +24,10 @@ import java.lang.annotation.Target;
/**
* Annotation for a method invocation that is retryable.
*
*
* @author Dave Syer
* @since 2.0
* @author Artem Bilan
* @since 1.1
*
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@@ -34,10 +35,16 @@ import java.lang.annotation.Target;
@Documented
public @interface Retryable {
/**
* Retry interceptor bean name to be applied for retryable method.
* Is mutually exclusive with other attributes.
* @return the retry interceptor bean name
*/
String interceptor() default "";
/**
* Exception types that are retryable. Synonym for includes(). Defaults to
* empty (and if excludes is also empty all exceptions are retried).
*
* @return exception types to retry
*/
Class<? extends Throwable>[] value() default {};
@@ -45,7 +52,6 @@ public @interface Retryable {
/**
* Exception types that are retryable. Defaults to empty (and if excludes is
* also empty all exceptions are retried).
*
* @return exception types to retry
*/
Class<? extends Throwable>[] include() default {};
@@ -53,7 +59,6 @@ public @interface Retryable {
/**
* Exception types that are not retryable. Defaults to empty (and if
* includes is also empty all exceptions are retried).
*
* @return exception types to retry
*/
Class<? extends Throwable>[] exclude() default {};
@@ -63,7 +68,6 @@ public @interface Retryable {
* but the retry policy is applied with the same policy to subsequent
* invocations with the same arguments. If false then retryable exceptions
* are not re-thrown.
*
* @return true if retry is stateful, default false
*/
boolean stateful() default false;
@@ -78,7 +82,6 @@ public @interface Retryable {
* Specify the backof properties for retrying this operation. The default is
* no backoff, but it can be a good idea to pause between attempts (even at
* the cost of blocking a thread).
*
* @return a backoff specification
*/
Backoff backoff() default @Backoff();

View File

@@ -0,0 +1,294 @@
/*
* Copyright 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.interceptor;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.retry.RetryOperations;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
/**
* <p>Simplified facade to make it easier and simpler to build a
* {@link StatefulRetryOperationsInterceptor} or
* (stateless) {@link RetryOperationsInterceptor}
* by providing a fluent interface to defining the behavior on error.
* <p>
* Typical example:
* </p>
*
* <pre class="code">
* StatefulRetryOperationsInterceptor interceptor =
* RetryInterceptorBuilder.stateful()
* .maxAttempts(5)
* .backOffOptions(1, 2, 10) // initialInterval, multiplier, maxInterval
* .build();
* </pre>
*
* @author James Carr
* @author Gary Russell
* @author Artem Bilan
* @since 1.1
*
* @param <T> The type of {@link org.aopalliance.intercept.MethodInterceptor}
* returned by the builder's {@link #build()} method.
*/
public abstract class RetryInterceptorBuilder<T extends MethodInterceptor> {
protected final RetryTemplate retryTemplate = new RetryTemplate();
protected final SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
protected RetryOperations retryOperations;
protected MethodInvocationRecoverer<?> recoverer;
private boolean templateAltered;
private boolean backOffPolicySet;
private boolean retryPolicySet;
private boolean backOffOptionsSet;
/**
* Create a builder for a stateful retry interceptor.
* @return The interceptor builder.
*/
public static StatefulRetryInterceptorBuilder stateful() {
return new StatefulRetryInterceptorBuilder();
}
/**
* Create a builder for a stateless retry interceptor.
* @return The interceptor builder.
*/
public static StatelessRetryInterceptorBuilder stateless() {
return new StatelessRetryInterceptorBuilder();
}
/**
* Apply the retry operations - once this is set, other properties can no longer be set; can't
* be set if other properties have been applied.
* @param retryOperations The retry operations.
* @return this.
*/
public RetryInterceptorBuilder<T> retryOperations(RetryOperations retryOperations) {
Assert.isTrue(!this.templateAltered, "Cannot set retryOperations when the default has been modified");
this.retryOperations = retryOperations;
return this;
}
/**
* Apply the max attempts - a SimpleRetryPolicy will be used. Cannot be used if a custom retry operations
* or retry policy has been set.
* @param maxAttempts the max attempts.
* @return this.
*/
public RetryInterceptorBuilder<T> maxAttempts(int maxAttempts) {
Assert.isNull(this.retryOperations, "cannot alter the retry policy when a custom retryOperations has been set");
Assert.isTrue(!this.retryPolicySet, "cannot alter the retry policy when a custom retryPolicy has been set");
this.simpleRetryPolicy.setMaxAttempts(maxAttempts);
this.retryTemplate.setRetryPolicy(this.simpleRetryPolicy);
this.templateAltered = true;
return this;
}
/**
* Apply the backoff options. Cannot be used if a custom retry operations, or back off policy has been set.
* @param initialInterval The initial interval.
* @param multiplier The multiplier.
* @param maxInterval The max interval.
* @return this.
*/
public RetryInterceptorBuilder<T> backOffOptions(long initialInterval, double multiplier, long maxInterval) {
Assert.isNull(this.retryOperations, "cannot set the back off policy when a custom retryOperations has been set");
Assert.isTrue(!this.backOffPolicySet, "cannot set the back off options when a back off policy has been set");
ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy();
policy.setInitialInterval(initialInterval);
policy.setMultiplier(multiplier);
policy.setMaxInterval(maxInterval);
this.retryTemplate.setBackOffPolicy(policy);
this.backOffOptionsSet = true;
this.templateAltered = true;
return this;
}
/**
* Apply the retry policy - cannot be used if a custom retry template has been provided, or the max attempts or
* back off options or policy have been applied.
* @param policy The policy.
* @return this.
*/
public RetryInterceptorBuilder<T> retryPolicy(RetryPolicy policy) {
Assert.isNull(this.retryOperations, "cannot set the retry policy when a custom retryOperations has been set");
Assert.isTrue(!this.templateAltered, "cannot set the retry policy if max attempts or back off policy or options changed");
this.retryTemplate.setRetryPolicy(policy);
this.retryPolicySet = true;
this.templateAltered = true;
return this;
}
/**
* Apply the back off policy. Cannot be used if a custom retry operations, or back off policy has been applied.
* @param policy The policy.
* @return this.
*/
public RetryInterceptorBuilder<T> backOffPolicy(BackOffPolicy policy) {
Assert.isNull(this.retryOperations, "cannot set the back off policy when a custom retryOperations has been set");
Assert.isTrue(!this.backOffOptionsSet, "cannot set the back off policy when the back off policy options have been set");
this.retryTemplate.setBackOffPolicy(policy);
this.templateAltered = true;
this.backOffPolicySet = true;
return this;
}
/**
* Apply a {@link MethodInvocationRecoverer} for the Retry interceptor.
* @param recoverer The recoverer.
* @return this.
*/
public RetryInterceptorBuilder<T> recoverer(MethodInvocationRecoverer<?> recoverer) {
this.recoverer = recoverer;
return this;
}
public abstract T build();
private RetryInterceptorBuilder() {
}
public static class StatefulRetryInterceptorBuilder extends RetryInterceptorBuilder<StatefulRetryOperationsInterceptor> {
private final StatefulRetryOperationsInterceptor interceptor = new StatefulRetryOperationsInterceptor();
private MethodArgumentsKeyGenerator keyGenerator;
private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier;
/**
* Stateful retry requires items to be identifiable.
* @param keyGenerator The key generator.
* @return this.
*/
public StatefulRetryInterceptorBuilder keyGenerator(MethodArgumentsKeyGenerator keyGenerator) {
this.keyGenerator = keyGenerator;
return this;
}
/**
* Apply a custom new item identifier.
* @param newMethodArgumentsIdentifier The new item identifier.
* @return this.
*/
public StatefulRetryInterceptorBuilder newMethodArgumentsIdentifier(NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) {
this.newMethodArgumentsIdentifier = newMethodArgumentsIdentifier;
return this;
}
@Override
public StatefulRetryInterceptorBuilder retryOperations(
RetryOperations retryOperations) {
super.retryOperations(retryOperations);
return this;
}
@Override
public StatefulRetryInterceptorBuilder maxAttempts(int maxAttempts) {
super.maxAttempts(maxAttempts);
return this;
}
@Override
public StatefulRetryInterceptorBuilder backOffOptions(long initialInterval,
double multiplier, long maxInterval) {
super.backOffOptions(initialInterval, multiplier, maxInterval);
return this;
}
@Override
public StatefulRetryInterceptorBuilder retryPolicy(RetryPolicy policy) {
super.retryPolicy(policy);
return this;
}
@Override
public StatefulRetryInterceptorBuilder backOffPolicy(BackOffPolicy policy) {
super.backOffPolicy(policy);
return this;
}
@Override
public StatefulRetryInterceptorBuilder recoverer(MethodInvocationRecoverer<?> recoverer) {
super.recoverer(recoverer);
return this;
}
@Override
public StatefulRetryOperationsInterceptor build() {
if (this.recoverer != null) {
this.interceptor.setRecoverer(this.recoverer);
}
if (this.retryOperations != null) {
this.interceptor.setRetryOperations(this.retryOperations);
}
else {
this.interceptor.setRetryOperations(this.retryTemplate);
}
if (this.keyGenerator != null) {
this.interceptor.setKeyGenerator(this.keyGenerator);
}
if (this.newMethodArgumentsIdentifier != null) {
this.interceptor.setNewItemIdentifier(this.newMethodArgumentsIdentifier);
}
return this.interceptor;
}
private StatefulRetryInterceptorBuilder() {
}
}
public static class StatelessRetryInterceptorBuilder extends RetryInterceptorBuilder<RetryOperationsInterceptor> {
private final RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor();
@Override
public RetryOperationsInterceptor build() {
if (this.recoverer != null) {
this.interceptor.setRecoverer(this.recoverer);
}
if (this.retryOperations != null) {
this.interceptor.setRetryOperations(this.retryOperations);
}
else {
this.interceptor.setRetryOperations(this.retryTemplate);
}
return this.interceptor;
}
private StatelessRetryInterceptorBuilder() {
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 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.
@@ -21,16 +21,20 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.aopalliance.intercept.MethodInterceptor;
import org.junit.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.backoff.Sleeper;
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
/**
* @author Dave Syer
*
* @author Artem Bilan
* @since 1.1
*/
public class EnableRetryTests {
@@ -92,7 +96,8 @@ public class EnableRetryTests {
try {
service.service();
fail("Expected IllegalStateException");
} catch (IllegalStateException e) {
}
catch (IllegalStateException e) {
}
assertEquals(1, service.getCount());
context.close();
@@ -106,7 +111,8 @@ public class EnableRetryTests {
for (int i = 0; i < 3; i++) {
try {
service.service(1);
} catch (Exception e) {
}
catch (Exception e) {
assertEquals("Planned", e.getMessage());
}
}
@@ -114,6 +120,15 @@ public class EnableRetryTests {
context.close();
}
@Test
public void testExternalInterceptor() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
InterceptableService service = context.getBean(InterceptableService.class);
service.service();
assertEquals(5, service.getCount());
context.close();
}
@Configuration
@EnableRetry(proxyTargetClass = true)
protected static class TestProxyConfiguration {
@@ -163,6 +178,17 @@ public class EnableRetryTests {
return new ExcludesService();
}
@Bean
public MethodInterceptor retryInterceptor() {
return RetryInterceptorBuilder.stateless()
.maxAttempts(5)
.build();
}
@Bean
public InterceptableService serviceWithExternalInterceptor() {
return new InterceptableService();
}
}
protected static class Service {
@@ -185,6 +211,7 @@ public class EnableRetryTests {
protected static class RecoverableService {
private int count = 0;
private Throwable cause;
@Retryable(RuntimeException.class)
@@ -258,4 +285,21 @@ public class EnableRetryTests {
}
}
private static class InterceptableService {
private int count = 0;
@Retryable(interceptor = "retryInterceptor")
public void service() {
if (count++ < 4) {
throw new RuntimeException("Planned");
}
}
public int getCount() {
return count;
}
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright 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.interceptor;
import static org.junit.Assert.*;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.aopalliance.intercept.MethodInterceptor;
import org.junit.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.retry.RetryOperations;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.retry.util.test.TestUtils;
/**
* @author Gary Russell
* @author Artem Bilan
* @since 1.1
*
*/
public class RetryInterceptorBuilderTests {
@Test
public void testBasic() {
StatefulRetryOperationsInterceptor interceptor = RetryInterceptorBuilder.stateful().build();
assertEquals(3, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
}
@Test
public void testWithCustomRetryTemplate() {
RetryOperations retryOperations = new RetryTemplate();
StatefulRetryOperationsInterceptor interceptor = RetryInterceptorBuilder.stateful()
.retryOperations(retryOperations)
.build();
assertEquals(3, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
assertSame(retryOperations, TestUtils.getPropertyValue(interceptor, "retryOperations"));
}
@Test
public void testWithMoreAttempts() {
StatefulRetryOperationsInterceptor interceptor =
RetryInterceptorBuilder.stateful()
.maxAttempts(5)
.build();
assertEquals(5, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
}
@Test
public void testWithCustomizedBackOffMoreAttempts() {
StatefulRetryOperationsInterceptor interceptor =
RetryInterceptorBuilder.stateful()
.maxAttempts(5)
.backOffOptions(1, 2, 10)
.build();
assertEquals(5, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
assertEquals(1L, TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.initialInterval"));
assertEquals(2.0, TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.multiplier"));
assertEquals(10L, TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.maxInterval"));
}
@Test
public void testWithCustomBackOffPolicy() {
StatefulRetryOperationsInterceptor interceptor =
RetryInterceptorBuilder.stateful()
.maxAttempts(5)
.backOffPolicy(new FixedBackOffPolicy())
.build();
assertEquals(5, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
assertEquals(1000L, TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod"));
}
@Test
public void testWithCustomNewMessageIdentifier() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
StatefulRetryOperationsInterceptor interceptor =
RetryInterceptorBuilder.stateful()
.maxAttempts(5)
.newMethodArgumentsIdentifier(new NewMethodArgumentsIdentifier() {
@Override
public boolean isNew(Object[] args) {
latch.countDown();
return false;
}
})
.backOffPolicy(new FixedBackOffPolicy())
.build();
assertEquals(5, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
assertEquals(1000L, TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod"));
final AtomicInteger count = new AtomicInteger();
Foo delegate = createDelegate(interceptor, count);
Object message = "";
try {
delegate.onMessage("", message);
}
catch (RuntimeException e) {
assertEquals("foo", e.getMessage());
}
assertEquals(1, count.get());
assertTrue(latch.await(0, TimeUnit.SECONDS));
}
@Test
public void testWitCustomRetryPolicyTraverseCause() {
StatefulRetryOperationsInterceptor interceptor = RetryInterceptorBuilder.stateful()
.retryPolicy(new SimpleRetryPolicy(15, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true), true))
.build();
assertEquals(15, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
}
@Test
public void testWithCustomKeyGenerator() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
StatefulRetryOperationsInterceptor interceptor = RetryInterceptorBuilder.stateful()
.keyGenerator(new MethodArgumentsKeyGenerator() {
@Override
public Object getKey(Object[] item) {
latch.countDown();
return "foo";
}
})
.build();
assertEquals(3, TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts"));
final AtomicInteger count = new AtomicInteger();
Foo delegate = createDelegate(interceptor, count);
Object message = "";
try {
delegate.onMessage("", message);
}
catch (RuntimeException e) {
assertEquals("foo", e.getMessage());
}
assertEquals(1, count.get());
assertTrue(latch.await(0, TimeUnit.SECONDS));
}
private Foo createDelegate(MethodInterceptor interceptor, final AtomicInteger count) {
Foo delegate = new Foo() {
@Override
public void onMessage(String s, Object message) {
count.incrementAndGet();
throw new RuntimeException("foo", new RuntimeException("bar"));
}
};
ProxyFactory factory = new ProxyFactory();
factory.addAdvisor(new DefaultPointcutAdvisor(Pointcut.TRUE, interceptor));
factory.setProxyTargetClass(false);
factory.addInterface(Foo.class);
factory.setTarget(delegate);
delegate = (Foo) factory.getProxy();
return delegate;
}
static interface Foo {
void onMessage(String s, Object message);
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 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.util.test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.util.Assert;
/**
* See Spring Integration TestUtils.
* @author Mark Fisher
* @author Iwein Fuld
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 1.2
*/
public class TestUtils {
/**
* Uses nested {@link org.springframework.beans.DirectFieldAccessor}s to obtain a property using dotted notation
* to traverse fields; e.g.
* "foo.bar.baz" will obtain a reference to the baz field of the bar field of foo. Adopted from Spring Integration.
* @param root The object.
* @param propertyPath The path.
* @return The field.
*/
public static Object getPropertyValue(Object root, String propertyPath) {
Object value = null;
DirectFieldAccessor accessor = new DirectFieldAccessor(root);
String[] tokens = propertyPath.split("\\.");
for (int i = 0; i < tokens.length; i++) {
value = accessor.getPropertyValue(tokens[i]);
if (value != null) {
accessor = new DirectFieldAccessor(value);
}
else if (i == tokens.length - 1) {
return null;
}
else {
throw new IllegalArgumentException("intermediate property '" + tokens[i] + "' is null");
}
}
return value;
}
@SuppressWarnings("unchecked")
public static <T> T getPropertyValue(Object root, String propertyPath, Class<T> type) {
Object value = getPropertyValue(root, propertyPath);
if (value != null) {
Assert.isAssignable(type, value.getClass());
}
return (T) value;
}
}