Mastering Spring Retry: Deep Dive into Retry and Back‑off Strategies
This article explains Spring Retry's architecture, how @EnableRetry, @Retryable, @Backoff and @Recover work together, details the available retry and back‑off policies, and walks through the core implementation using RetryTemplate and interceptors to provide flexible, AOP‑based retry handling in Java backend applications.
Overview
Spring provides a simple yet powerful retry mechanism. Spring Retry, extracted from Spring Batch, is widely used in Spring Batch, Spring Integration, Spring for Apache Hadoop and other projects. This article explains how to use Spring Retry and its implementation principles.
Background
Retry is often needed to ensure fault tolerance, availability, and consistency, especially when dealing with unpredictable external system responses, exceptions, network latency, or interruptions. In micro‑service governance frameworks like Dubbo, retry and timeout configurations are common. Manually coding retry logic mixes it with business code, making maintenance hard; therefore, retry should be abstracted.
Usage Introduction
Basic Usage
Example:
@Configuration
@EnableRetry
public class Application {
@Bean
public RetryService retryService() {
return new RetryService();
}
public static void main(String[] args) throws Exception {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext("springretry");
RetryService service1 = applicationContext.getBean("service", RetryService.class);
service1.service();
}
}
@Service("service")
public class RetryService {
@Retryable(value = IllegalAccessException.class, maxAttempts = 5,
backoff = @Backoff(value = 1500, maxDelay = 100000, multiplier = 1.2))
public void service() throws IllegalAccessException {
System.out.println("service method...");
throw new IllegalAccessException("manual exception");
}
@Recover
public void recover(IllegalAccessException e) {
System.out.println("service retry after Recover => " + e.getMessage());
}
}@EnableRetry enables the retry mechanism. @Retryable marks a method for retry with many configurable parameters. @Backoff defines the back‑off strategy. @Recover provides a fallback method when all retries fail.
Spring‑Retry offers rich features such as various retry policies, back‑off policies, recover callbacks, and listeners.
Retry Strategies
SimpleRetryPolicy – default max 3 attempts
TimeoutRetryPolicy – retries failures within 1 second
ExpressionRetryPolicy – retries when an expression matches
CircuitBreakerRetryPolicy – adds circuit‑breaker behavior
CompositeRetryPolicy – combines multiple policies
NeverRetryPolicy – never retries
AlwaysRetryPolicy – always retries
Back‑off Strategies
FixedBackOffPolicy – fixed delay (default 1 s)
ExponentialBackOffPolicy – exponential increase (initial 0.1 s, multiplier 2, max 30 s)
ExponentialRandomBackOffPolicy – exponential with randomness
UniformRandomBackOffPolicy – random delay within a fixed range
StatelessBackOffPolicy – stateless back‑off
Implementation Principles
Entry Point – @EnableRetry
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@Import(RetryConfiguration.class)
@Documented
public @interface EnableRetry {
/**
* Indicate whether subclass‑based (CGLIB) proxies are to be created as opposed
* to standard Java interface‑based proxies. The default is {@code false}.
*
* @return whether to proxy the class
*/
boolean proxyTargetClass() default false;
}@EnableRetry imports RetryConfiguration, which registers an AbstractPointcutAdvisor containing a pointcut and an advice. The pointcut matches methods annotated with @Retryable.
RetryConfiguration
During bean initialization it builds the pointcut for @Retryable and creates an AnnotationAwareRetryOperationsInterceptor as the advice.
AnnotationAwareRetryOperationsInterceptor
This class implements MethodInterceptor. Its invoke method delegates to a stateful or stateless interceptor based on configuration.
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
MethodInterceptor delegate = getDelegate(invocation.getThis(), invocation.getMethod());
if (delegate != null) {
return delegate.invoke(invocation);
} else {
return invocation.proceed();
}
}Stateless Interceptor Creation
private MethodInterceptor getStatelessInterceptor(Object target, Method method, Retryable retryable) {
// generate a RetryTemplate
RetryTemplate template = createTemplate(retryable.listeners());
// set retryPolicy
template.setRetryPolicy(getRetryPolicy(retryable));
// set backOffPolicy
template.setBackOffPolicy(getBackoffPolicy(retryable.backoff()));
return RetryInterceptorBuilder.stateless()
.retryOperations(template)
.label(retryable.label())
.recoverer(getRecoverer(target, method))
.build();
}It builds a RetryTemplate, sets the appropriate RetryPolicy and BackOffPolicy, and configures a recoverer.
RetryPolicy Construction
private RetryPolicy getRetryPolicy(Annotation retryable) {
Map<String, Object> attrs = AnnotationUtils.getAnnotationAttributes(retryable);
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) {
includes = (Class<? extends Throwable>[]) attrs.get("include");
}
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(resolve(maxAttemptsExpression), PARSER_CONTEXT)
.getValue(evaluationContext, Integer.class);
}
if (includes.length == 0 && excludes.length == 0) {
SimpleRetryPolicy simple = hasExpression ?
new ExpressionRetryPolicy(resolve(exceptionExpression)).withBeanFactory(beanFactory) :
new SimpleRetryPolicy();
simple.setMaxAttempts(maxAttempts);
return simple;
}
Map<Class<? extends Throwable>, Boolean> policyMap = new HashMap<>();
for (Class<? extends Throwable> type : includes) {
policyMap.put(type, true);
}
for (Class<? extends Throwable> type : excludes) {
policyMap.put(type, false);
}
boolean retryNotExcluded = includes.length == 0;
if (hasExpression) {
return new ExpressionRetryPolicy(maxAttempts, policyMap, true, exceptionExpression, retryNotExcluded)
.withBeanFactory(beanFactory);
} else {
return new SimpleRetryPolicy(maxAttempts, policyMap, true, retryNotExcluded);
}
}Based on @Retryable attributes it creates SimpleRetryPolicy, ExpressionRetryPolicy or a composite policy, handling include/exclude exception lists and max attempts.
BackOffPolicy Construction
private BackOffPolicy getBackoffPolicy(Backoff backoff) {
long min = backoff.delay() == 0 ? backoff.value() : backoff.delay();
if (StringUtils.hasText(backoff.delayExpression())) {
min = PARSER.parseExpression(resolve(backoff.delayExpression()), PARSER_CONTEXT)
.getValue(evaluationContext, Long.class);
}
long max = backoff.maxDelay();
if (StringUtils.hasText(backoff.maxDelayExpression())) {
max = PARSER.parseExpression(resolve(backoff.maxDelayExpression()), PARSER_CONTEXT)
.getValue(evaluationContext, Long.class);
}
double multiplier = backoff.multiplier();
if (StringUtils.hasText(backoff.multiplierExpression())) {
multiplier = PARSER.parseExpression(resolve(backoff.multiplierExpression()), PARSER_CONTEXT)
.getValue(evaluationContext, Double.class);
}
if (multiplier > 0) {
ExponentialBackOffPolicy policy = backoff.random() ? new ExponentialRandomBackOffPolicy() : new ExponentialBackOffPolicy();
policy.setInitialInterval(min);
policy.setMultiplier(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;
}It interprets @Backoff attributes to create ExponentialBackOffPolicy, UniformRandomBackOffPolicy, or FixedBackOffPolicy instances.
Core Retry Logic (RetryTemplate.doExecute)
protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,
RecoveryCallback<T> recoveryCallback, RetryState state) throws E, ExhaustedRetryException {
RetryPolicy retryPolicy = this.retryPolicy;
BackOffPolicy backOffPolicy = this.backOffPolicy;
RetryContext context = open(retryPolicy, state);
RetrySynchronizationManager.register(context);
Throwable lastException = null;
boolean exhausted = false;
try {
boolean running = doOpenInterceptors(retryCallback, context);
if (!running) {
throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt");
}
BackOffContext backOffContext = null;
Object resource = context.getAttribute("backOffContext");
if (resource instanceof BackOffContext) {
backOffContext = (BackOffContext) resource;
}
if (backOffContext == null) {
backOffContext = backOffPolicy.start(context);
if (backOffContext != null) {
context.setAttribute("backOffContext", backOffContext);
}
}
while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
try {
lastException = null;
return retryCallback.doWithRetry(context);
} catch (Throwable e) {
lastException = e;
try {
registerThrowable(retryPolicy, state, context, e);
} catch (Exception ex) {
throw new TerminatedRetryException("Could not register throwable", ex);
} finally {
doOnErrorInterceptors(retryCallback, context, e);
}
if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
try {
backOffPolicy.backOff(backOffContext);
} catch (BackOffInterruptedException ex) {
lastException = e;
throw ex;
}
if (shouldRethrow(retryPolicy, context, state)) {
throw RetryTemplate.<E>wrapIfNecessary(e);
}
}
if (state != null && context.hasAttribute(RetrySynchronizationManager.GLOBAL_STATE)) {
break;
}
}
}
exhausted = true;
return handleRetryExhausted(recoveryCallback, context, state);
} catch (Throwable e) {
throw RetryTemplate.<E>wrapIfNecessary(e);
} finally {
close(retryPolicy, context, state, lastException == null || exhausted);
doCloseInterceptors(retryCallback, context, lastException);
RetrySynchronizationManager.clear();
}
}The template opens a RetryContext, invokes listeners, checks RetryPolicy.canRetry, executes the target method, applies the back‑off, and finally calls a recovery callback or rethrows the exception.
RetryContext
Each retry operation has its own RetryContext which stores retry count, last exception, and back‑off state. Context objects are cached to avoid creating many policy instances.
Summary
Spring Retry leverages AOP to inject retry behavior into business code. The core logic resides in RetryTemplate, while a variety of retry and back‑off policies provide flexible configuration.
References
http://www.10tiao.com/html/164/201705/2652898434/1.html
https://www.jianshu.com/p/58e753ca0151
https://paper.tuisec.win/detail/90bd660fad92183
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
