diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java b/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java
index 107c818583d..c575746b027 100644
--- a/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java
+++ b/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java
@@ -16,6 +16,8 @@
package org.springframework.core.retry;
+import org.jspecify.annotations.Nullable;
+
import org.springframework.core.retry.support.CompositeRetryListener;
/**
@@ -42,7 +44,7 @@ public interface RetryListener {
* @param retryExecution the retry execution
* @param result the result of the {@link Retryable}
*/
- default void onRetrySuccess(RetryExecution retryExecution, Object result) {
+ default void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) {
}
/**
diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java
index 04e6da98123..6b806fd4d2d 100644
--- a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java
+++ b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java
@@ -17,14 +17,13 @@
package org.springframework.core.retry;
import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
-import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogAccessor;
-import org.springframework.core.retry.support.CompositeRetryListener;
import org.springframework.core.retry.support.MaxRetryAttemptsPolicy;
import org.springframework.util.Assert;
import org.springframework.util.backoff.BackOff;
@@ -48,6 +47,7 @@ import org.springframework.util.backoff.FixedBackOff;
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
+ * @author Juergen Hoeller
* @since 7.0
* @see RetryOperations
* @see RetryPolicy
@@ -57,14 +57,13 @@ import org.springframework.util.backoff.FixedBackOff;
*/
public class RetryTemplate implements RetryOperations {
- protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
+ private static final LogAccessor logger = new LogAccessor(RetryTemplate.class);
- protected RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy();
+ private RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy();
- protected BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1));
+ private BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1));
- protected RetryListener retryListener = new RetryListener() {
- };
+ private RetryListener retryListener = new RetryListener() {};
/**
@@ -121,7 +120,8 @@ public class RetryTemplate implements RetryOperations {
/**
* Set the {@link RetryListener} to use.
- *
If multiple listeners are needed, use a {@link CompositeRetryListener}.
+ *
If multiple listeners are needed, use a
+ * {@link org.springframework.core.retry.support.CompositeRetryListener}.
*
Defaults to a no-op implementation.
* @param retryListener the retry listener to use
*/
@@ -158,10 +158,26 @@ public class RetryTemplate implements RetryOperations {
// Retry process starts here
RetryExecution retryExecution = this.retryPolicy.start();
BackOffExecution backOffExecution = this.backOffPolicy.start();
- List suppressedExceptions = new ArrayList<>();
+ Deque exceptions = new ArrayDeque<>();
+ exceptions.add(initialException);
Throwable retryException = initialException;
while (retryExecution.shouldRetry(retryException)) {
+ try {
+ long duration = backOffExecution.nextBackOff();
+ if (duration == BackOffExecution.STOP) {
+ break;
+ }
+ logger.debug(() -> "Backing off for %dms after retryable operation '%s'"
+ .formatted(duration, retryableName));
+ Thread.sleep(duration);
+ }
+ catch (InterruptedException interruptedException) {
+ Thread.currentThread().interrupt();
+ throw new RetryException(
+ "Unable to back off for retryable operation '%s'".formatted(retryableName),
+ interruptedException);
+ }
logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName));
try {
this.retryListener.beforeRetry(retryExecution);
@@ -172,29 +188,22 @@ public class RetryTemplate implements RetryOperations {
return result;
}
catch (Throwable currentAttemptException) {
+ logger.debug(() -> "Retry attempt for operation '%s' failed due to '%s'"
+ .formatted(retryableName, currentAttemptException));
this.retryListener.onRetryFailure(retryExecution, currentAttemptException);
- try {
- long duration = backOffExecution.nextBackOff();
- logger.debug(() -> "Retryable operation '%s' failed due to '%s'; backing off for %dms"
- .formatted(retryableName, currentAttemptException.getMessage(), duration));
- Thread.sleep(duration);
- }
- catch (InterruptedException interruptedException) {
- Thread.currentThread().interrupt();
- throw new RetryException(
- "Unable to back off for retryable operation '%s'".formatted(retryableName),
- interruptedException);
- }
- suppressedExceptions.add(currentAttemptException);
+ exceptions.add(currentAttemptException);
retryException = currentAttemptException;
}
}
+
// The RetryPolicy has exhausted at this point, so we throw a RetryException with the
// initial exception as the cause and remaining exceptions as suppressed exceptions.
RetryException finalException = new RetryException(
"Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName),
- initialException);
- suppressedExceptions.forEach(finalException::addSuppressed);
+ exceptions.removeLast());
+ for (Iterator it = exceptions.descendingIterator(); it.hasNext();) {
+ finalException.addSuppressed(it.next());
+ }
this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException);
throw finalException;
}
diff --git a/spring-core/src/main/java/org/springframework/core/retry/Retryable.java b/spring-core/src/main/java/org/springframework/core/retry/Retryable.java
index fffc34dc00d..21460b4537b 100644
--- a/spring-core/src/main/java/org/springframework/core/retry/Retryable.java
+++ b/spring-core/src/main/java/org/springframework/core/retry/Retryable.java
@@ -16,6 +16,8 @@
package org.springframework.core.retry;
+import org.jspecify.annotations.Nullable;
+
/**
* {@code Retryable} is a functional interface that can be used to implement any
* generic block of code that can potentially be retried.
@@ -36,7 +38,7 @@ public interface Retryable {
* @return the result of the operation
* @throws Throwable if an error occurs during the execution of the operation
*/
- R execute() throws Throwable;
+ @Nullable R execute() throws Throwable;
/**
* A unique, logical name for this retryable operation, used to distinguish
diff --git a/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java b/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java
index f97a04125b9..8f7d99fa1bd 100644
--- a/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java
+++ b/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java
@@ -19,6 +19,8 @@ package org.springframework.core.retry.support;
import java.util.LinkedList;
import java.util.List;
+import org.jspecify.annotations.Nullable;
+
import org.springframework.core.retry.RetryExecution;
import org.springframework.core.retry.RetryListener;
import org.springframework.core.retry.RetryTemplate;
@@ -26,11 +28,10 @@ import org.springframework.util.Assert;
/**
* A composite implementation of the {@link RetryListener} interface.
+ * Delegate listeners will be called in their registration order.
*
* This class is used to compose multiple listeners within a {@link RetryTemplate}.
*
- *
Delegate listeners will be called in their registration order.
- *
* @author Mahmoud Ben Hassine
* @since 7.0
*/
@@ -63,13 +64,14 @@ public class CompositeRetryListener implements RetryListener {
this.listeners.add(listener);
}
+
@Override
public void beforeRetry(RetryExecution retryExecution) {
this.listeners.forEach(retryListener -> retryListener.beforeRetry(retryExecution));
}
@Override
- public void onRetrySuccess(RetryExecution retryExecution, Object result) {
+ public void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) {
this.listeners.forEach(listener -> listener.onRetrySuccess(retryExecution, result));
}