Add Expression Support to @Retryable

Resolves GH-34
This commit is contained in:
Gary Russell
2015-12-16 16:29:01 -05:00
parent 8c8877deac
commit ba35d97222
6 changed files with 354 additions and 13 deletions

View File

@@ -274,6 +274,34 @@ class Service {
}
```
Version 1.2 introduces the ability to use expressions for certain properties:
```java
@Retryable(exceptionExpression="#{message.contains('this can be retried')}")
public void service1() {
...
}
@Retryable(exceptionExpression="#{message.contains('this can be retried')}")
public void service2() {
...
}
@Retryable(exceptionExpression="#{@exceptionChecker.shouldRetry(#root)}",
maxAttemptsExpression = "#{@integerFiveBean}",
backoff = @Backoff(delayExpression = "#{1}", maxDelayExpression = "#{5}", multiplierExpression = "#{1.1}"))
public void service3() {
...
}
```
These use the familier Spring SpEL expression syntax (`#{...}`).
- `exceptionExpression` is evaluated against the thrown exception as the `#root` object.
- `maxAttemptsExpression` and the `@BackOff` expression attributes are evaluated once, during initialization; there is no root object for the evaluation but they can reference other beans in the context.
### XML Configuration
Here is an example of declarative iteration using Spring AOP to repeat a service call to a method called `remoteCall` (for more detail on how to configure AOP interceptors see the Spring User Guide):

View File

@@ -26,12 +26,17 @@ 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.context.expression.BeanFactoryResolver;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.BackOffPolicy;
@@ -47,6 +52,7 @@ import org.springframework.retry.interceptor.MethodInvocationRecoverer;
import org.springframework.retry.interceptor.NewMethodArgumentsIdentifier;
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
import org.springframework.retry.policy.CircuitBreakerRetryPolicy;
import org.springframework.retry.policy.ExpressionRetryPolicy;
import org.springframework.retry.policy.MapRetryContextCache;
import org.springframework.retry.policy.RetryContextCache;
import org.springframework.retry.policy.SimpleRetryPolicy;
@@ -67,6 +73,12 @@ import org.springframework.util.StringUtils;
*/
public class AnnotationAwareRetryOperationsInterceptor implements IntroductionInterceptor, BeanFactoryAware {
private static final TemplateParserContext PARSER_CONTEXT = new TemplateParserContext();
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
private final Map<Method, MethodInterceptor> delegates = new HashMap<Method, MethodInterceptor>();
private RetryContextCache retryContextCache = new MapRetryContextCache();
@@ -113,7 +125,7 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
/**
* Retry listeners to apply to all operations.
* @param listeners the listeners
* @param listeners the listeners
*/
public void setListeners(Collection<RetryListener> listeners) {
ArrayList<RetryListener> retryListeners = new ArrayList<RetryListener>(listeners);
@@ -124,6 +136,7 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
this.evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
@Override
@@ -263,6 +276,8 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
Map<String, Object> attrs = AnnotationUtils.getAnnotationAttributes(retryable);
@SuppressWarnings("unchecked")
Class<? extends Throwable>[] includes = (Class<? extends Throwable>[]) attrs.get("value");
String exceptionExpression = (String) attrs.get("exceptionExpression");
boolean hasExpression = StringUtils.hasText(exceptionExpression);
if (includes.length == 0) {
@SuppressWarnings("unchecked")
Class<? extends Throwable>[] value = (Class<? extends Throwable>[]) attrs.get("include");
@@ -270,9 +285,17 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
}
@SuppressWarnings("unchecked")
Class<? extends Throwable>[] excludes = (Class<? extends Throwable>[]) attrs.get("exclude");
Integer maxAttempts = (Integer) attrs.get("maxAttempts");
String maxAttemptsExpression = (String) attrs.get("maxAttemptsExpression");
if (StringUtils.hasText(maxAttemptsExpression)) {
maxAttempts = PARSER.parseExpression(maxAttemptsExpression, PARSER_CONTEXT).getValue(this.evaluationContext,
Integer.class);
}
if (includes.length == 0 && excludes.length == 0) {
SimpleRetryPolicy simple = new SimpleRetryPolicy();
simple.setMaxAttempts((Integer) attrs.get("maxAttempts"));
SimpleRetryPolicy simple = hasExpression ? new ExpressionRetryPolicy(exceptionExpression)
.withBeanFactory(this.beanFactory)
: new SimpleRetryPolicy();
simple.setMaxAttempts(maxAttempts);
return simple;
}
Map<Class<? extends Throwable>, Boolean> policyMap = new HashMap<Class<? extends Throwable>, Boolean>();
@@ -282,19 +305,38 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
for (Class<? extends Throwable> type : excludes) {
policyMap.put(type, false);
}
return new SimpleRetryPolicy((Integer) attrs.get("maxAttempts"), policyMap, true);
if (hasExpression) {
return new ExpressionRetryPolicy(maxAttempts, policyMap, true, exceptionExpression)
.withBeanFactory(this.beanFactory);
}
else {
return new SimpleRetryPolicy(maxAttempts, policyMap, true);
}
}
private BackOffPolicy getBackoffPolicy(Backoff backoff) {
long min = backoff.delay() == 0 ? backoff.value() : backoff.delay();
if (StringUtils.hasText(backoff.delayExpression())) {
min = PARSER.parseExpression(backoff.delayExpression(), PARSER_CONTEXT).getValue(this.evaluationContext,
Long.class);
}
long max = backoff.maxDelay();
if (backoff.multiplier() > 0) {
if (StringUtils.hasText(backoff.maxDelayExpression())) {
max = PARSER.parseExpression(backoff.maxDelayExpression(), PARSER_CONTEXT).getValue(this.evaluationContext,
Long.class);
}
double multiplier = backoff.multiplier();
if (StringUtils.hasText(backoff.multiplierExpression())) {
multiplier = PARSER.parseExpression(backoff.multiplierExpression(), PARSER_CONTEXT)
.getValue(this.evaluationContext, Double.class);
}
if (multiplier > 0) {
ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy();
if (backoff.random()) {
policy = new ExponentialRandomBackOffPolicy();
}
policy.setInitialInterval(min);
policy.setMultiplier(backoff.multiplier());
policy.setMultiplier(multiplier);
policy.setMaxInterval(max > min ? max : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL);
if (this.sleeper != null) {
policy.setSleeper(this.sleeper);

View File

@@ -27,7 +27,7 @@ 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>
@@ -38,9 +38,10 @@ import org.springframework.retry.backoff.BackOffPolicy;
* <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
* @since 2.0
* @author Gary Russell
* @since 1.1
*
*/
@Target(ElementType.TYPE)
@@ -51,7 +52,7 @@ public @interface Backoff {
/**
* Synonym for {@link #delay()}.
*
*
* @return the delay in milliseconds (default 1000)
*/
long value() default 1000;
@@ -66,24 +67,53 @@ public @interface Backoff {
/**
* 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;
/**
* An expression evaluating to the canonical backoff period. Used as an initial value
* in the exponential case, and as a minimum value in the uniform case.
* Overrides {@link #delay()}.
* @return the initial or canonical backoff period in milliseconds.
* @since 1.2
*/
String delayExpression() default "";
/**
* An expression evaluating to the maximimum wait (in milliseconds) between retries.
* If less than the {@link #delay()} then ignored.
* Overrides {@link #maxDelay()}
*
* @return the maximum delay between retries (default 0 = ignored)
* @since 1.2
*/
String maxDelayExpression() default "";
/**
* Evaluates to a vaule used as a multiplier for generating the next delay for backoff.
* Overrides {@link #multiplier()}.
*
* @return a multiplier expression to use to calculate the next backoff delay (default 0 =
* ignored)
* @since 1.2
*/
String multiplierExpression() default "";
/**
* In the exponential case ({@link #multiplier()} &gt; 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

@@ -27,6 +27,7 @@ import java.lang.annotation.Target;
*
* @author Dave Syer
* @author Artem Bilan
* @author Gary Russell
* @since 1.1
*
*/
@@ -84,6 +85,13 @@ public @interface Retryable {
*/
int maxAttempts() default 3;
/**
* @return an expression evaluated to the maximum number of attempts (including the first failure), defaults to 3
* Overrides {@link #maxAttempts()}.
* @since 1.2
*/
String maxAttemptsExpression() default "";
/**
* Specify the backoff 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
@@ -92,4 +100,22 @@ public @interface Retryable {
*/
Backoff backoff() default @Backoff();
/**
* Specify an expression to be evaluated after the {@code SimpleRetryPolicy.canRetry()}
* returns true - can be used to conditionally suppress the retry. Only invoked after
* an exception is thrown. The root object for the evaluation is the last {@code Throwable}.
* Other beans in the context can be referenced.
* For example:
* <pre class=code>
* {@code "message.contains('you can retry this')"}.
* </pre>
* and
* <pre class=code>
* {@code "@someBean.shouldRetry(#root)"}.
* </pre>
* @return the expression.
* @since 1.2
*/
String exceptionExpression() default "";
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2015 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.policy;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.retry.RetryContext;
import org.springframework.util.Assert;
/**
* Subclass of {@link SimpleRetryPolicy} that delegates to super.canRetry() and,
* if true, further evaluates an expression against the last thrown exception.
*
* @author Gary Russell
* @since 1.2
*
*/
@SuppressWarnings("serial")
public class ExpressionRetryPolicy extends SimpleRetryPolicy implements BeanFactoryAware {
private static final TemplateParserContext PARSER_CONTEXT = new TemplateParserContext();
private final Expression expression;
private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
/**
* Construct an instance with the provided {@link Expression}.
* @param expression the expression
*/
public ExpressionRetryPolicy(Expression expression) {
Assert.notNull(expression, "'expression' cannot be null");
this.expression = expression;
}
/**
* Construct an instance with the provided expression.
* @param expressionString the expression.
*/
public ExpressionRetryPolicy(String expressionString) {
Assert.notNull(expressionString, "'expressionString' cannot be null");
this.expression = new SpelExpressionParser().parseExpression(expressionString, PARSER_CONTEXT);
}
/**
* Construct an instance with the provided {@link Expression}.
* @param maxAttempts the max attempts
* @param retryableExceptions the exceptions
* @param traverseCauses true to examine causes
* @param expression the expression
*/
public ExpressionRetryPolicy(int maxAttempts, Map<Class<? extends Throwable>, Boolean> retryableExceptions,
boolean traverseCauses, Expression expression) {
super(maxAttempts, retryableExceptions, traverseCauses);
Assert.notNull(expression, "'expression' cannot be null");
this.expression = expression;
}
/**
* Construct an instance with the provided expression.
* @param maxAttempts the max attempts
* @param retryableExceptions the exceptions
* @param traverseCauses true to examine causes
* @param expressionString the expression.
*/
public ExpressionRetryPolicy(int maxAttempts, Map<Class<? extends Throwable>, Boolean> retryableExceptions,
boolean traverseCauses, String expressionString) {
super(maxAttempts, retryableExceptions, traverseCauses);
Assert.notNull(expressionString, "'expressionString' cannot be null");
this.expression = new SpelExpressionParser().parseExpression(expressionString, PARSER_CONTEXT);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
public ExpressionRetryPolicy withBeanFactory(BeanFactory beanFactory) {
setBeanFactory(beanFactory);
return this;
}
@Override
public boolean canRetry(RetryContext context) {
Throwable lastThrowable = context.getLastThrowable();
if (lastThrowable == null) {
return super.canRetry(context);
}
else {
return super.canRetry(context)
&& this.expression.getValue(this.evaluationContext, lastThrowable, Boolean.class);
}
}
}

View File

@@ -22,15 +22,22 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.reflect.Method;
import java.util.Map;
import org.aopalliance.intercept.MethodInterceptor;
import org.junit.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.backoff.Sleeper;
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
/**
* @author Dave Syer
@@ -156,6 +163,43 @@ public class EnableRetryTests {
context.close();
}
@Test
public void testExpression() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
ExpressionService service = context.getBean(ExpressionService.class);
service.service1();
assertEquals(3, service.getCount());
try {
service.service2();
fail("expected exception");
}
catch (RuntimeException e) {
assertEquals("this cannot be retried", e.getMessage());
}
assertEquals(4, service.getCount());
service.service3();
assertEquals(9, service.getCount());
RetryConfiguration config = context.getBean(RetryConfiguration.class);
AnnotationAwareRetryOperationsInterceptor advice =
(AnnotationAwareRetryOperationsInterceptor) new DirectFieldAccessor(config).getPropertyValue("advice");
@SuppressWarnings("unchecked")
Map<Method, MethodInterceptor> delegates = (Map<Method, MethodInterceptor>) new DirectFieldAccessor(advice)
.getPropertyValue("delegates");
MethodInterceptor interceptor = delegates
.get(ExpressionService.class.getDeclaredMethod("service3"));
RetryTemplate template = (RetryTemplate) new DirectFieldAccessor(interceptor)
.getPropertyValue("retryOperations");
DirectFieldAccessor templateAccessor = new DirectFieldAccessor(template);
ExponentialBackOffPolicy backOff = (ExponentialBackOffPolicy) templateAccessor
.getPropertyValue("backOffPolicy");
assertEquals(1, backOff.getInitialInterval());
assertEquals(5, backOff.getMaxInterval());
assertEquals(1.1, backOff.getMultiplier(), 0.1);
SimpleRetryPolicy retryPolicy = (SimpleRetryPolicy) templateAccessor.getPropertyValue("retryPolicy");
assertEquals(5, retryPolicy.getMaxAttempts());
context.close();
}
@Configuration
@EnableRetry(proxyTargetClass = true)
protected static class TestProxyConfiguration {
@@ -223,6 +267,21 @@ public class EnableRetryTests {
return new InterceptableService();
}
@Bean
public ExpressionService expressionService() {
return new ExpressionService();
}
@Bean
public ExceptionChecker exceptionChecker() {
return new ExceptionChecker();
}
@Bean
public Integer integerFiveBean() {
return Integer.valueOf(5);
}
@Bean
public Foo foo() {
return new Foo();
@@ -371,6 +430,46 @@ public class EnableRetryTests {
}
private static class ExpressionService {
private int count = 0;
@Retryable(exceptionExpression="#{message.contains('this can be retried')}")
public void service1() {
if (count++ < 2) {
throw new RuntimeException("this can be retried");
}
}
@Retryable(exceptionExpression="#{message.contains('this can be retried')}")
public void service2() {
count++;
throw new RuntimeException("this cannot be retried");
}
@Retryable(exceptionExpression="#{@exceptionChecker.shouldRetry(#root)}",
maxAttemptsExpression = "#{@integerFiveBean}",
backoff = @Backoff(delayExpression = "#{1}", maxDelayExpression = "#{5}", multiplierExpression = "#{1.1}"))
public void service3() {
if (count++ < 8) {
throw new RuntimeException();
}
}
public int getCount() {
return count;
}
}
public static class ExceptionChecker {
public boolean shouldRetry(Throwable t) {
return true;
}
}
private static class Foo {
}