diff --git a/pom.xml b/pom.xml
index 26cdd02..2171eac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0org.springframework.retryspring-retry
- 1.0.4.BUILD-SNAPSHOT
+ 1.1.0.BUILD-SNAPSHOTSpring Retry
@@ -21,7 +21,7 @@
jartrue
- 3.0.5.RELEASE
+ 4.0.3.RELEASE
@@ -123,14 +123,14 @@
- spring-release
+ repo.spring.ioSpring Release Repository
- s3://maven.springframework.org/release
+ https://repo.spring.io/libs-release-local
- spring-snapshot
+ repo.spring.ioSpring Snapshot Repository
- s3://maven.springframework.org/snapshot
+ https://repo.spring.io/libs-snapshot-local
@@ -138,7 +138,7 @@
junitjunit
- 4.8.2
+ 4.11test
@@ -156,7 +156,7 @@
org.aspectjaspectjweaver
- 1.6.6
+ 1.7.4test
@@ -185,22 +185,7 @@
-
-
- com.springsource.repository.bundles.release
- http://repository.springsource.com/maven/bundles/release
-
-
-
-
-
-
- org.springframework.build.aws
- org.springframework.build.aws.maven
- 3.0.0.RELEASE
-
-
@@ -233,8 +218,8 @@
org.apache.maven.pluginsmaven-compiler-plugin
- 1.5
- 1.5
+ 1.7
+ 1.7
@@ -250,20 +235,6 @@
-
- com.springsource.bundlor
- com.springsource.bundlor.maven
- 1.0.0.RELEASE
- true
-
-
- bundlor
-
- bundlor
-
-
-
- maven-javadoc-plugin
diff --git a/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java
index 18e09f7..942b1c6 100644
--- a/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java
+++ b/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java
@@ -16,18 +16,18 @@
package org.springframework.retry.backoff;
-
/**
- * Implementation of {@link BackOffPolicy} that pauses for a fixed period of
- * time before continuing. A pause is implemented using {@link Thread#sleep(long)}.
- * {@link #setBackOffPeriod(long)} is thread-safe and it is safe to call
- * {@link #setBackOffPeriod} during execution from multiple threads, however
- * this may cause a single retry operation to have pauses of different
- * intervals.
+ * Implementation of {@link BackOffPolicy} that pauses for a fixed period of time before
+ * continuing. A pause is implemented using {@link Sleeper#sleep(long)}.
+ *
+ * {@link #setBackOffPeriod(long)} is thread-safe and it is safe to call
+ * {@link #setBackOffPeriod} during execution from multiple threads, however this may
+ * cause a single retry operation to have pauses of different intervals.
* @author Rob Harrop
* @author Dave Syer
*/
-public class FixedBackOffPolicy extends StatelessBackOffPolicy implements SleepingBackOffPolicy {
+public class FixedBackOffPolicy extends StatelessBackOffPolicy implements
+ SleepingBackOffPolicy {
/**
* Default back off period - 1000ms.
@@ -39,15 +39,14 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi
*/
private volatile long backOffPeriod = DEFAULT_BACK_OFF_PERIOD;
-
private Sleeper sleeper = new ObjectWaitSleeper();
- public FixedBackOffPolicy withSleeper(Sleeper sleeper) {
- FixedBackOffPolicy res = new FixedBackOffPolicy();
- res.setBackOffPeriod(backOffPeriod);
- res.setSleeper(sleeper);
- return res;
- }
+ public FixedBackOffPolicy withSleeper(Sleeper sleeper) {
+ FixedBackOffPolicy res = new FixedBackOffPolicy();
+ res.setBackOffPeriod(backOffPeriod);
+ res.setSleeper(sleeper);
+ return res;
+ }
/**
* Public setter for the {@link Sleeper} strategy.
@@ -58,8 +57,7 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi
}
/**
- * Set the back off period in milliseconds. Cannot be < 1. Default value
- * is 1000ms.
+ * Set the back off period in milliseconds. Cannot be < 1. Default value is 1000ms.
*/
public void setBackOffPeriod(long backOffPeriod) {
this.backOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1);
@@ -80,13 +78,12 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi
protected void doBackOff() throws BackOffInterruptedException {
try {
sleeper.sleep(backOffPeriod);
- }
- catch (InterruptedException e) {
+ } catch (InterruptedException e) {
throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
}
}
- public String toString() {
- return "FixedBackOffPolicy[backOffPeriod=" + backOffPeriod + "]";
- }
+ public String toString() {
+ return "FixedBackOffPolicy[backOffPeriod=" + backOffPeriod + "]";
+ }
}
diff --git a/src/main/java/org/springframework/retry/backoff/UniformRandomBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/UniformRandomBackOffPolicy.java
new file mode 100644
index 0000000..5005ae5
--- /dev/null
+++ b/src/main/java/org/springframework/retry/backoff/UniformRandomBackOffPolicy.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+package org.springframework.retry.backoff;
+
+import java.util.Random;
+
+
+/**
+ * Implementation of {@link BackOffPolicy} that pauses for a random period of
+ * time before continuing. A pause is implemented using {@link Sleeper#sleep(long)}.
+ * {@link #setMinBackOffPeriod(long)} is thread-safe and it is safe to call
+ * {@link #setBackOffPeriod} during execution from multiple threads, however
+ * this may cause a single retry operation to have pauses of different
+ * intervals.
+ * @author Rob Harrop
+ * @author Dave Syer
+ */
+public class UniformRandomBackOffPolicy extends StatelessBackOffPolicy implements SleepingBackOffPolicy {
+
+ /**
+ * Default min back off period - 500ms.
+ */
+ private static final long DEFAULT_BACK_OFF_MIN_PERIOD = 500L;
+
+ /**
+ * Default max back off period - 1500ms.
+ */
+ private static final long DEFAULT_BACK_OFF_MAX_PERIOD = 1500L;
+
+ private volatile long minBackOffPeriod = DEFAULT_BACK_OFF_MIN_PERIOD;
+
+ private volatile long maxBackOffPeriod = DEFAULT_BACK_OFF_MAX_PERIOD;
+
+ private Random random = new Random(System.currentTimeMillis());
+
+ private Sleeper sleeper = new ObjectWaitSleeper();
+
+ public UniformRandomBackOffPolicy withSleeper(Sleeper sleeper) {
+ UniformRandomBackOffPolicy res = new UniformRandomBackOffPolicy();
+ res.setMinBackOffPeriod(minBackOffPeriod);
+ res.setSleeper(sleeper);
+ return res;
+ }
+
+ /**
+ * Public setter for the {@link Sleeper} strategy.
+ * @param sleeper the sleeper to set defaults to {@link ObjectWaitSleeper}.
+ */
+ public void setSleeper(Sleeper sleeper) {
+ this.sleeper = sleeper;
+ }
+
+ /**
+ * Set the minimum back off period in milliseconds. Cannot be < 1. Default value
+ * is 500ms.
+ */
+ public void setMinBackOffPeriod(long backOffPeriod) {
+ this.minBackOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1);
+ }
+
+ /**
+ * The minimum backoff period in milliseconds.
+ * @return the backoff period
+ */
+ public long getMinBackOffPeriod() {
+ return minBackOffPeriod;
+ }
+
+ /**
+ * Set the maximum back off period in milliseconds. Cannot be < 1. Default value
+ * is 1500ms.
+ */
+ public void setMaxBackOffPeriod(long backOffPeriod) {
+ this.maxBackOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1);
+ }
+
+ /**
+ * The maximum backoff period in milliseconds.
+ * @return the backoff period
+ */
+ public long getMaxBackOffPeriod() {
+ return maxBackOffPeriod;
+ }
+
+ /**
+ * Pause for the {@link #setMinBackOffPeriod(long)}.
+ * @throws BackOffInterruptedException if interrupted during sleep.
+ */
+ protected void doBackOff() throws BackOffInterruptedException {
+ try {
+ long delta = maxBackOffPeriod==minBackOffPeriod ? 0 : random.nextInt((int) (maxBackOffPeriod - minBackOffPeriod));
+ sleeper.sleep(minBackOffPeriod + delta );
+ }
+ catch (InterruptedException e) {
+ throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
+ }
+ }
+
+ public String toString() {
+ return "RandomBackOffPolicy[backOffPeriod=" + minBackOffPeriod + ", " + maxBackOffPeriod + "]";
+ }
+}
diff --git a/src/main/java/org/springframework/retry/config/AnnotationAwareRetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/config/AnnotationAwareRetryOperationsInterceptor.java
new file mode 100644
index 0000000..edda8fc
--- /dev/null
+++ b/src/main/java/org/springframework/retry/config/AnnotationAwareRetryOperationsInterceptor.java
@@ -0,0 +1,202 @@
+/*
+ * 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.config;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.retry.RetryPolicy;
+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.interceptor.MethodArgumentsKeyGenerator;
+import org.springframework.retry.interceptor.NewMethodArgumentsIdentifier;
+import org.springframework.retry.interceptor.RetryOperationsInterceptor;
+import org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor;
+import org.springframework.retry.policy.MapRetryContextCache;
+import org.springframework.retry.policy.RetryContextCache;
+import org.springframework.retry.policy.SimpleRetryPolicy;
+import org.springframework.retry.support.RetryTemplate;
+
+/**
+ * Wrapper interceptor that interprets the retry metadata on the method it is invoking and
+ * delegates to an appropriate RetryOperationsInterceptor.
+ *
+ * @author Dave Syer
+ *
+ */
+public class AnnotationAwareRetryOperationsInterceptor implements MethodInterceptor {
+
+ private Map delegates = new HashMap();
+
+ private RetryContextCache retryContextCache = new MapRetryContextCache();
+
+ private MethodArgumentsKeyGenerator methodArgumentsKeyGenerator;
+
+ private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier;
+
+ private Sleeper sleeper;
+
+ /**
+ * @param sleeper the sleeper to set
+ */
+ public void setSleeper(Sleeper sleeper) {
+ this.sleeper = sleeper;
+ }
+
+ /**
+ * Public setter for the {@link RetryContextCache}.
+ *
+ * @param retryContextCache the {@link RetryContextCache} to set.
+ */
+ public void setRetryContextCache(RetryContextCache retryContextCache) {
+ this.retryContextCache = retryContextCache;
+ }
+
+ /**
+ * @param methodArgumentsKeyGenerator
+ */
+ public void setKeyGenerator(MethodArgumentsKeyGenerator methodArgumentsKeyGenerator) {
+ this.methodArgumentsKeyGenerator = methodArgumentsKeyGenerator;
+ }
+
+ /**
+ * @param newMethodArgumentsIdentifier
+ */
+ public void setNewItemIdentifier(
+ NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) {
+ this.newMethodArgumentsIdentifier = newMethodArgumentsIdentifier;
+ }
+
+ public Object invoke(MethodInvocation invocation) throws Throwable {
+ MethodInterceptor delegate = getDelegate(invocation.getMethod());
+ return delegate.invoke(invocation);
+ }
+
+ private MethodInterceptor getDelegate(Method method) {
+ if (!delegates.containsKey(method)) {
+ synchronized (delegates) {
+ if (!delegates.containsKey(method)) {
+ Retryable retryable = AnnotationUtils.findAnnotation(method,
+ Retryable.class);
+ if (retryable == null) {
+ retryable = AnnotationUtils.findAnnotation(
+ method.getDeclaringClass(), Retryable.class);
+ }
+ MethodInterceptor delegate;
+ if (retryable.stateful()) {
+ delegate = getStatefulInterceptor(retryable);
+ } else {
+ delegate = getStatelessInterceptor(retryable);
+ }
+ // TODO: allow declaring class to specify @Recover methods
+ delegates.put(method, delegate);
+ }
+ }
+ }
+ return delegates.get(method);
+ }
+
+ private MethodInterceptor getStatelessInterceptor(Retryable retryable) {
+ RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor();
+ RetryTemplate template = new RetryTemplate();
+ template.setRetryPolicy(getRetryPolicy(retryable));
+ template.setBackOffPolicy(getBackoffPolicy(retryable.backoff()));
+ interceptor.setRetryOperations(template);
+ return interceptor;
+ }
+
+ private MethodInterceptor getStatefulInterceptor(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.setRetryPolicy(getRetryPolicy(retryable));
+ template.setBackOffPolicy(getBackoffPolicy(retryable.backoff()));
+ interceptor.setRetryOperations(template);
+ return interceptor;
+ }
+
+ private RetryPolicy getRetryPolicy(Retryable retryable) {
+ Class extends Throwable>[] includes = retryable.value();
+ if (includes.length == 0) {
+ includes = retryable.include();
+ }
+ Class extends Throwable>[] excludes = retryable.exclude();
+ if (includes.length == 0 && excludes.length == 0) {
+ SimpleRetryPolicy simple = new SimpleRetryPolicy();
+ simple.setMaxAttempts(retryable.maxAttempts());
+ return simple;
+ }
+ Map, Boolean> policyMap = new HashMap, Boolean>();
+ for (Class extends Throwable> type : includes) {
+ policyMap.put(type, true);
+ }
+ for (Class extends Throwable> type : excludes) {
+ policyMap.put(type, false);
+ }
+ SimpleRetryPolicy simple = new SimpleRetryPolicy(retryable.maxAttempts(),
+ policyMap, true);
+ return simple;
+ }
+
+ private BackOffPolicy getBackoffPolicy(Backoff backoff) {
+ long min = backoff.delay()==0 ? backoff.value() : backoff.delay();
+ long max = backoff.maxDelay();
+ 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.setSleeper(sleeper);
+ }
+ return policy;
+ }
+ if (max>min) {
+ UniformRandomBackOffPolicy policy = new UniformRandomBackOffPolicy();
+ policy.setMinBackOffPeriod(min);
+ policy.setMaxBackOffPeriod(max);
+ if (sleeper!=null) {
+ policy.setSleeper(sleeper);
+ }
+ return policy;
+ }
+ FixedBackOffPolicy policy = new FixedBackOffPolicy();
+ policy.setBackOffPeriod(min);
+ if (sleeper!=null) {
+ policy.setSleeper(sleeper);
+ }
+ return policy;
+ }
+
+}
diff --git a/src/main/java/org/springframework/retry/config/Backoff.java b/src/main/java/org/springframework/retry/config/Backoff.java
new file mode 100644
index 0000000..073d48f
--- /dev/null
+++ b/src/main/java/org/springframework/retry/config/Backoff.java
@@ -0,0 +1,90 @@
+/*
+ * 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.config;
+
+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;
+import org.springframework.retry.backoff.BackOffPolicy;
+
+/**
+ * Collects metadata for a {@link BackOffPolicy}. Features:
+ *
+ *
+ *
With no explicit settings the default is a fixed delay of 1000ms
+ *
Only the {@link #delay()} set: the backoff is a fixed delay with that value
+ *
When {@link #delay()} and {@link #maxDelay()} are set the backoff is uniformly
+ * distributed between the two values
+ *
With {@link #delay()}, {@link #maxDelay()} and {@link #multiplier()} the backoff is
+ * exponentially growing up to the maximum value
+ *
If, in addition, the {@link #random()} flag is set then the multiplier is chosen
+ * for each delay from a uniform distribution in [1, multiplier-1]
+ *
+ *
+ * @author Dave Syer
+ *
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Import(RetryConfiguration.class)
+@Documented
+public @interface Backoff {
+
+ /**
+ * Synonym for {@link #delay()}.
+ *
+ * @return the delay in milliseconds (default 1000)
+ */
+ long value() default 1000;
+
+ /**
+ * A canonical backoff period. Used as an initial value in the exponential case, and
+ * as a minimum value in the uniform case.
+ * @return the initial or canonical backoff period in milliseconds (default 1000)
+ */
+ long delay() default 0;
+
+ /**
+ * The maximimum wait (in milliseconds) between retries. If less than the
+ * {@link #delay()} then ignored.
+ *
+ * @return the maximum delay between retries (default 0 = ignored)
+ */
+ long maxDelay() default 0;
+
+ /**
+ * If positive, then used as a multiplier for generating the next delay for backoff.
+ *
+ * @return a multiplier to use to calculate the next backoff delay (default 0 =
+ * ignored)
+ */
+ double multiplier() default 0;
+
+ /**
+ * In the exponential case ({@link #multiplier()}>0) set this to true to have the
+ * backoff delays randomized, so that the maximum delay is multiplier times the
+ * previous delay and the distribution is uniform between the two values.
+ *
+ * @return the flag to signal randomization is required (default false)
+ */
+ boolean random() default false;
+
+}
diff --git a/src/main/java/org/springframework/retry/config/EnableRetry.java b/src/main/java/org/springframework/retry/config/EnableRetry.java
new file mode 100644
index 0000000..99f1e91
--- /dev/null
+++ b/src/main/java/org/springframework/retry/config/EnableRetry.java
@@ -0,0 +1,42 @@
+/*
+ * 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.config;
+
+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;
+
+/**
+ * Global enabler for @Retryable annotations in Spring beans. If this is
+ * declared on any @Configuration in the context then beans that have
+ * retryable methods will be proxied and the retry handled according to the metadata in
+ * the annotations.
+ *
+ * @author Dave Syer
+ *
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Import(RetryConfiguration.class)
+@Documented
+public @interface EnableRetry {
+
+}
diff --git a/src/main/java/org/springframework/retry/config/RetryConfiguration.java b/src/main/java/org/springframework/retry/config/RetryConfiguration.java
new file mode 100644
index 0000000..6f3502b
--- /dev/null
+++ b/src/main/java/org/springframework/retry/config/RetryConfiguration.java
@@ -0,0 +1,135 @@
+/*
+ * 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.config;
+
+import java.lang.annotation.Annotation;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import javax.annotation.PostConstruct;
+
+import org.aopalliance.aop.Advice;
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.support.AbstractPointcutAdvisor;
+import org.springframework.aop.support.ComposablePointcut;
+import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.retry.backoff.Sleeper;
+import org.springframework.retry.interceptor.MethodArgumentsKeyGenerator;
+import org.springframework.retry.interceptor.NewMethodArgumentsIdentifier;
+import org.springframework.retry.policy.RetryContextCache;
+
+/**
+ * Basic configuration for @Retryable processing. For stateful retry, if
+ * there is a unique bean elsewhere in the context of type {@link RetryContextCache},
+ * {@link MethodArgumentsKeyGenerator} or {@link NewMethodArgumentsIdentifier} it will be
+ * used by the corresponding retry interceptor (otherwise sensible defaults are adopted).
+ *
+ * @author Dave Syer
+ *
+ */
+@SuppressWarnings("serial")
+@Configuration
+public class RetryConfiguration extends AbstractPointcutAdvisor implements
+ BeanFactoryAware {
+
+ private Advice advice;
+
+ private Pointcut pointcut;
+
+ @Autowired(required = false)
+ private RetryContextCache retryContextCache;
+
+ @Autowired(required = false)
+ private MethodArgumentsKeyGenerator methodArgumentsKeyGenerator;
+
+ @Autowired(required = false)
+ private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier;
+
+ @Autowired(required = false)
+ private Sleeper sleeper;
+
+ @PostConstruct
+ public void init() {
+ Set> retryableAnnotationTypes = new LinkedHashSet>(
+ 1);
+ retryableAnnotationTypes.add(Retryable.class);
+ this.pointcut = buildPointcut(retryableAnnotationTypes);
+ this.advice = buildAdvice();
+ }
+
+ /**
+ * Set the {@code BeanFactory} to be used when looking up executors by qualifier.
+ */
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) {
+ if (this.advice instanceof BeanFactoryAware) {
+ ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
+ }
+ }
+
+ @Override
+ public Advice getAdvice() {
+ return this.advice;
+ }
+
+ @Override
+ public Pointcut getPointcut() {
+ return this.pointcut;
+ }
+
+ protected Advice buildAdvice() {
+ AnnotationAwareRetryOperationsInterceptor interceptor = new AnnotationAwareRetryOperationsInterceptor();
+ if (retryContextCache != null) {
+ interceptor.setRetryContextCache(retryContextCache);
+ }
+ if (methodArgumentsKeyGenerator != null) {
+ interceptor.setKeyGenerator(methodArgumentsKeyGenerator);
+ }
+ if (newMethodArgumentsIdentifier != null) {
+ interceptor.setNewItemIdentifier(newMethodArgumentsIdentifier);
+ }
+ if (sleeper != null) {
+ interceptor.setSleeper(sleeper);
+ }
+ return interceptor;
+ }
+
+ /**
+ * 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) {
+ ComposablePointcut result = null;
+ for (Class extends Annotation> retryAnnotationType : retryAnnotationTypes) {
+ Pointcut cpc = new AnnotationMatchingPointcut(retryAnnotationType, true);
+ Pointcut mpc = AnnotationMatchingPointcut
+ .forMethodAnnotation(retryAnnotationType);
+ if (result == null) {
+ result = new ComposablePointcut(cpc).union(mpc);
+ } else {
+ result.union(cpc).union(mpc);
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/springframework/retry/config/Retryable.java b/src/main/java/org/springframework/retry/config/Retryable.java
new file mode 100644
index 0000000..cc3b8dc
--- /dev/null
+++ b/src/main/java/org/springframework/retry/config/Retryable.java
@@ -0,0 +1,79 @@
+/*
+ * 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.config;
+
+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 retryable.
+ *
+ * @author Dave Syer
+ *
+ */
+@Target({ ElementType.METHOD, ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Import(RetryConfiguration.class)
+@Documented
+public @interface Retryable {
+
+ /**
+ * 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 {};
+
+ /**
+ * 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 {};
+
+ /**
+ * 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 {};
+
+ /**
+ * Flag to say that the retry is stateful: i.e. exceptions are re-thrown, 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;
+
+ /**
+ * @return the maximum number of attempts (including the first failure), defaults to 3
+ */
+ int maxAttempts() default 3;
+
+ Backoff backoff() default @Backoff();
+
+}
diff --git a/src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java b/src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java
index c7ef498..bc76774 100644
--- a/src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java
+++ b/src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java
@@ -84,7 +84,7 @@ public class ExceptionClassifierRetryPolicy implements RetryPolicy {
}
/**
- * Create an active context that proxies a retry policy by chosing a target
+ * Create an active context that proxies a retry policy by choosing a target
* from the policy map.
*
* @see org.springframework.retry.RetryPolicy#open(RetryContext)
diff --git a/src/main/java/org/springframework/retry/support/RetrySimulator.java b/src/main/java/org/springframework/retry/support/RetrySimulator.java
index e848e34..3a5fb9d 100644
--- a/src/main/java/org/springframework/retry/support/RetrySimulator.java
+++ b/src/main/java/org/springframework/retry/support/RetrySimulator.java
@@ -102,7 +102,8 @@ public class RetrySimulator {
}
}
- static class FailingRetryException extends Exception {
+ @SuppressWarnings("serial")
+ static class FailingRetryException extends Exception {
}
static class StealingSleeper implements Sleeper {
diff --git a/src/test/java/org/springframework/retry/AnyThrowTests.java b/src/test/java/org/springframework/retry/AnyThrowTests.java
new file mode 100644
index 0000000..db5245c
--- /dev/null
+++ b/src/test/java/org/springframework/retry/AnyThrowTests.java
@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class AnyThrowTests {
+
+ @Rule
+ public ExpectedException expected = ExpectedException.none();
+
+ @Test
+ public void testRuntimeException() throws Throwable {
+ expected.expect(RuntimeException.class);
+ AnyThrow.throwAny(new RuntimeException("planned"));
+ }
+
+ @Test
+ public void testUncheckedRuntimeException() throws Throwable {
+ expected.expect(RuntimeException.class);
+ AnyThrow.throwUnchecked(new RuntimeException("planned"));
+ }
+
+ @Test
+ public void testCheckedException() throws Throwable {
+ expected.expect(Exception.class);
+ AnyThrow.throwAny(new Exception("planned"));
+ }
+
+ private static class AnyThrow {
+
+ private static void throwUnchecked(Throwable e) {
+ AnyThrow.throwAny(e);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void throwAny(Throwable e) throws E {
+ throw (E)e;
+ }
+ }
+
+}
diff --git a/src/test/java/org/springframework/retry/config/EnableRetryTests.java b/src/test/java/org/springframework/retry/config/EnableRetryTests.java
new file mode 100644
index 0000000..c69bb13
--- /dev/null
+++ b/src/test/java/org/springframework/retry/config/EnableRetryTests.java
@@ -0,0 +1,180 @@
+/*
+ * 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.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+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.config.EnableRetry;
+import org.springframework.retry.config.Retryable;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class EnableRetryTests {
+
+ @Test
+ public void vanilla() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ TestConfiguration.class);
+ Service service = context.getBean(Service.class);
+ service.service();
+ assertEquals(3, service.getCount());
+ context.close();
+ }
+
+ @Test
+ public void type() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ TestConfiguration.class);
+ RetryableService service = context.getBean(RetryableService.class);
+ service.service();
+ assertEquals(3, service.getCount());
+ context.close();
+ }
+
+ @Test
+ public void excludes() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ TestConfiguration.class);
+ ExcludesService service = context.getBean(ExcludesService.class);
+ try {
+ service.service();
+ fail("Expected IllegalStateException");
+ } catch (IllegalStateException e) {
+ }
+ assertEquals(1, service.getCount());
+ context.close();
+ }
+
+ @Test
+ public void stateful() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ TestConfiguration.class);
+ StatefulService service = context.getBean(StatefulService.class);
+ for (int i = 0; i < 3; i++) {
+ try {
+ service.service(1);
+ } catch (Exception e) {
+ assertEquals("Planned", e.getMessage());
+ }
+ }
+ assertEquals(3, service.getCount());
+ context.close();
+ }
+
+ @Configuration
+ @EnableRetry
+ @EnableAspectJAutoProxy(proxyTargetClass = true)
+ protected static class TestConfiguration {
+
+ @Bean
+ public Service service() {
+ return new Service();
+ }
+
+ @Bean
+ public RetryableService retryable() {
+ return new RetryableService();
+ }
+
+ @Bean
+ public StatefulService stateful() {
+ return new StatefulService();
+ }
+
+ @Bean
+ public ExcludesService excludes() {
+ return new ExcludesService();
+ }
+
+ }
+
+ protected static class Service {
+
+ private int count = 0;
+
+ @Retryable(RuntimeException.class)
+ public void service() {
+ if (count++ < 2) {
+ throw new RuntimeException("Planned");
+ }
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ }
+
+ @Retryable(RuntimeException.class)
+ protected static class RetryableService {
+
+ private int count = 0;
+
+ public void service() {
+ if (count++ < 2) {
+ throw new RuntimeException("Planned");
+ }
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ }
+
+ protected static class ExcludesService {
+
+ private int count = 0;
+
+ @Retryable(include = RuntimeException.class, exclude = IllegalStateException.class)
+ public void service() {
+ if (count++ < 2) {
+ throw new IllegalStateException("Planned");
+ }
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ }
+
+ protected static class StatefulService {
+
+ private int count = 0;
+
+ @Retryable(stateful = true)
+ public void service(int value) {
+ if (count++ < 2) {
+ throw new RuntimeException("Planned");
+ }
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ }
+}
diff --git a/src/test/java/org/springframework/retry/config/EnableRetryWithBackoffTests.java b/src/test/java/org/springframework/retry/config/EnableRetryWithBackoffTests.java
new file mode 100644
index 0000000..88c889c
--- /dev/null
+++ b/src/test/java/org/springframework/retry/config/EnableRetryWithBackoffTests.java
@@ -0,0 +1,194 @@
+/*
+ * 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.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+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.backoff.Sleeper;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class EnableRetryWithBackoffTests {
+
+ @Test
+ public void vanilla() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ TestConfiguration.class);
+ Service service = context.getBean(Service.class);
+ service.service();
+ assertEquals("[1000, 1000]", context.getBean(TestConfiguration.class).periods.toString());
+ assertEquals(3, service.getCount());
+ context.close();
+ }
+
+ @Test
+ public void type() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ TestConfiguration.class);
+ RandomService service = context.getBean(RandomService.class);
+ service.service();
+ List periods = context.getBean(TestConfiguration.class).periods;
+ assertTrue("Wrong periods: " + periods, periods.get(0)>1000);
+ assertEquals(3, service.getCount());
+ context.close();
+ }
+
+ @Test
+ public void exponential() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ TestConfiguration.class);
+ ExponentialService service = context.getBean(ExponentialService.class);
+ service.service();
+ assertEquals(3, service.getCount());
+ assertEquals("[1000, 1100]", context.getBean(TestConfiguration.class).periods.toString());
+ context.close();
+ }
+
+ @Test
+ public void randomExponential() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ TestConfiguration.class);
+ ExponentialRandomService service = context
+ .getBean(ExponentialRandomService.class);
+ service.service(1);
+ assertEquals(3, service.getCount());
+ List periods = context.getBean(TestConfiguration.class).periods;
+ assertNotEquals("[1000, 1100]", context.getBean(TestConfiguration.class).periods.toString());
+ assertTrue("Wrong periods: " + periods, periods.get(0)>1000);
+ assertTrue("Wrong periods: " + periods, periods.get(1)>1100 && periods.get(1)<1210);
+ context.close();
+ }
+
+ @Configuration
+ @EnableRetry
+ @EnableAspectJAutoProxy(proxyTargetClass = true)
+ protected static class TestConfiguration {
+
+ private List periods = new ArrayList();
+
+ @Bean
+ public Sleeper sleper() {
+ return new Sleeper() {
+ @Override
+ public void sleep(long period) throws InterruptedException {
+ periods.add(period);
+ }
+ };
+ }
+
+ @Bean
+ public Service service() {
+ return new Service();
+ }
+
+ @Bean
+ public RandomService retryable() {
+ return new RandomService();
+ }
+
+ @Bean
+ public ExponentialRandomService stateful() {
+ return new ExponentialRandomService();
+ }
+
+ @Bean
+ public ExponentialService excludes() {
+ return new ExponentialService();
+ }
+
+ }
+
+ protected static class Service {
+
+ private int count = 0;
+
+ @Retryable(backoff = @Backoff(delay = 1000))
+ public void service() {
+ if (count++ < 2) {
+ throw new RuntimeException("Planned");
+ }
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ }
+
+ @Retryable(backoff = @Backoff(delay = 1000, maxDelay = 2000))
+ protected static class RandomService {
+
+ private int count = 0;
+
+ public void service() {
+ if (count++ < 2) {
+ throw new RuntimeException("Planned");
+ }
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ }
+
+ protected static class ExponentialService {
+
+ private int count = 0;
+
+ @Retryable(backoff = @Backoff(delay = 1000, maxDelay = 2000, multiplier = 1.1))
+ public void service() {
+ if (count++ < 2) {
+ throw new IllegalStateException("Planned");
+ }
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ }
+
+ protected static class ExponentialRandomService {
+
+ private int count = 0;
+
+ @Retryable(backoff = @Backoff(delay = 1000, maxDelay = 2000, multiplier = 1.1, random=true))
+ public void service(int value) {
+ if (count++ < 2) {
+ throw new RuntimeException("Planned");
+ }
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ }
+}
diff --git a/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java b/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java
index 1504108..be5088c 100644
--- a/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java
+++ b/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java
@@ -133,6 +133,7 @@ public class RetryOperationsInterceptorTests {
assertEquals(2, count);
// Expect 2 separate transactions...
assertEquals(2, transactionCount);
+ context.close();
}
@Test