diff --git a/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java index 3758a49..bc2fca2 100644 --- a/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java @@ -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 delegates = new HashMap(); + private final Map delegates = new HashMap(); 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 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; diff --git a/src/main/java/org/springframework/retry/annotation/RetryConfiguration.java b/src/main/java/org/springframework/retry/annotation/RetryConfiguration.java index e389363..ea2c045 100644 --- a/src/main/java/org/springframework/retry/annotation/RetryConfiguration.java +++ b/src/main/java/org/springframework/retry/annotation/RetryConfiguration.java @@ -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> retryableAnnotationTypes = new LinkedHashSet>( - 1); + Set> retryableAnnotationTypes = new LinkedHashSet>(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> retryAnnotationTypes) { + protected Pointcut buildPointcut(Set> retryAnnotationTypes) { ComposablePointcut result = null; for (Class 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); } } diff --git a/src/main/java/org/springframework/retry/annotation/Retryable.java b/src/main/java/org/springframework/retry/annotation/Retryable.java index da00e5a..990e129 100644 --- a/src/main/java/org/springframework/retry/annotation/Retryable.java +++ b/src/main/java/org/springframework/retry/annotation/Retryable.java @@ -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[] 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[] 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[] 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(); diff --git a/src/main/java/org/springframework/retry/interceptor/RetryInterceptorBuilder.java b/src/main/java/org/springframework/retry/interceptor/RetryInterceptorBuilder.java new file mode 100644 index 0000000..f354761 --- /dev/null +++ b/src/main/java/org/springframework/retry/interceptor/RetryInterceptorBuilder.java @@ -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; + +/** + *

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. + *

+ * Typical example: + *

+ * + *
+ *	StatefulRetryOperationsInterceptor interceptor =
+ *			RetryInterceptorBuilder.stateful()
+ *				.maxAttempts(5)
+ *				.backOffOptions(1, 2, 10) // initialInterval, multiplier, maxInterval
+ *				.build();
+ * 
+ * + * @author James Carr + * @author Gary Russell + * @author Artem Bilan + * @since 1.1 + * + * @param The type of {@link org.aopalliance.intercept.MethodInterceptor} + * returned by the builder's {@link #build()} method. + */ +public abstract class RetryInterceptorBuilder { + + 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 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 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 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 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 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 recoverer(MethodInvocationRecoverer recoverer) { + this.recoverer = recoverer; + return this; + } + + public abstract T build(); + + + private RetryInterceptorBuilder() { + } + + public static class StatefulRetryInterceptorBuilder extends RetryInterceptorBuilder { + + 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 { + + 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() { + } + + } + +} diff --git a/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java b/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java index a610d0c..b6cc391 100644 --- a/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java +++ b/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java @@ -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; + } + + } } diff --git a/src/test/java/org/springframework/retry/interceptor/RetryInterceptorBuilderTests.java b/src/test/java/org/springframework/retry/interceptor/RetryInterceptorBuilderTests.java new file mode 100644 index 0000000..f553879 --- /dev/null +++ b/src/test/java/org/springframework/retry/interceptor/RetryInterceptorBuilderTests.java @@ -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 + ., 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); + } + +} diff --git a/src/test/java/org/springframework/retry/util/test/TestUtils.java b/src/test/java/org/springframework/retry/util/test/TestUtils.java new file mode 100644 index 0000000..c085365 --- /dev/null +++ b/src/test/java/org/springframework/retry/util/test/TestUtils.java @@ -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 getPropertyValue(Object root, String propertyPath, Class type) { + Object value = getPropertyValue(root, propertyPath); + if (value != null) { + Assert.isAssignable(type, value.getClass()); + } + return (T) value; + } + +}