Add @EnableRetry support (like @Async)

This commit is contained in:
Dave Syer
2014-04-21 14:11:18 +01:00
parent 06f766a43f
commit 705eabcbf8
14 changed files with 1133 additions and 63 deletions

49
pom.xml
View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.0.4.BUILD-SNAPSHOT</version>
<version>1.1.0.BUILD-SNAPSHOT</version>
<name>Spring Retry</name>
<description><![CDATA[Spring Retry provides an abstraction around retrying failed operations, with an emphasis on declarative control of the process and policy-based bahaviour that is easy to extend and customize. For instance, you can configure a plain POJO operation to retry if it fails, based on the type of exception, and with a fixed or exponential backoff.
]]></description>
@@ -21,7 +21,7 @@
<packaging>jar</packaging>
<properties>
<maven.test.failure.ignore>true</maven.test.failure.ignore>
<spring.framework.version>3.0.5.RELEASE</spring.framework.version>
<spring.framework.version>4.0.3.RELEASE</spring.framework.version>
</properties>
<profiles>
<profile>
@@ -123,14 +123,14 @@
</url>
</site>
<repository>
<id>spring-release</id>
<id>repo.spring.io</id>
<name>Spring Release Repository</name>
<url>s3://maven.springframework.org/release</url>
<url>https://repo.spring.io/libs-release-local</url>
</repository>
<snapshotRepository>
<id>spring-snapshot</id>
<id>repo.spring.io</id>
<name>Spring Snapshot Repository</name>
<url>s3://maven.springframework.org/snapshot</url>
<url>https://repo.spring.io/libs-snapshot-local</url>
</snapshotRepository>
</distributionManagement>
@@ -138,7 +138,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -156,7 +156,7 @@
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.6</version>
<version>1.7.4</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -185,22 +185,7 @@
</dependency>
</dependencies>
<pluginRepositories>
<pluginRepository>
<id>com.springsource.repository.bundles.release</id>
<url>http://repository.springsource.com/maven/bundles/release</url>
</pluginRepository>
</pluginRepositories>
<build>
<extensions>
<extension>
<groupId>org.springframework.build.aws</groupId>
<artifactId>org.springframework.build.aws.maven</artifactId>
<version>3.0.0.RELEASE</version>
</extension>
</extensions>
<pluginManagement>
<plugins>
<plugin>
@@ -233,8 +218,8 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
@@ -250,20 +235,6 @@
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>com.springsource.bundlor</groupId>
<artifactId>com.springsource.bundlor.maven</artifactId>
<version>1.0.0.RELEASE</version>
<inherited>true</inherited>
<executions>
<execution>
<id>bundlor</id>
<goals>
<goal>bundlor</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>

View File

@@ -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)}.
* <p/> {@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)}.
* <p/>
* {@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<FixedBackOffPolicy> {
public class FixedBackOffPolicy extends StatelessBackOffPolicy implements
SleepingBackOffPolicy<FixedBackOffPolicy> {
/**
* 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 &lt; 1. Default value
* is 1000ms.
* Set the back off period in milliseconds. Cannot be &lt; 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 + "]";
}
}

View File

@@ -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)}.
* <p/> {@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<UniformRandomBackOffPolicy> {
/**
* 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 &lt; 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 &lt; 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 + "]";
}
}

View File

@@ -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<Method, MethodInterceptor> delegates = new HashMap<Method, MethodInterceptor>();
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<Class<? extends Throwable>, Boolean> policyMap = new HashMap<Class<? extends Throwable>, 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;
}
}

View File

@@ -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:
*
* <ul>
* <li>With no explicit settings the default is a fixed delay of 1000ms</li>
* <li>Only the {@link #delay()} set: the backoff is a fixed delay with that value</li>
* <li>When {@link #delay()} and {@link #maxDelay()} are set the backoff is uniformly
* distributed between the two values</li>
* <li>With {@link #delay()}, {@link #maxDelay()} and {@link #multiplier()} the backoff is
* exponentially growing up to the maximum value</li>
* <li>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]</li>
* </ul>
*
* @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;
}

View File

@@ -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 <code>@Retryable</code> annotations in Spring beans. If this is
* declared on any <code>@Configuration</code> 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 {
}

View File

@@ -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 <code>@Retryable</code> 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<Class<? extends Annotation>> retryableAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(
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<Class<? extends Annotation>> 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;
}
}

View File

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

View File

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

View File

@@ -102,7 +102,8 @@ public class RetrySimulator {
}
}
static class FailingRetryException extends Exception {
@SuppressWarnings("serial")
static class FailingRetryException extends Exception {
}
static class StealingSleeper implements Sleeper {

View File

@@ -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.<RuntimeException>throwAny(e);
}
@SuppressWarnings("unchecked")
private static <E extends Throwable> void throwAny(Throwable e) throws E {
throw (E)e;
}
}
}

View File

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

View File

@@ -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<Long> 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<Long> 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<Long> periods = new ArrayList<Long>();
@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;
}
}
}

View File

@@ -133,6 +133,7 @@ public class RetryOperationsInterceptorTests {
assertEquals(2, count);
// Expect 2 separate transactions...
assertEquals(2, transactionCount);
context.close();
}
@Test