diff --git a/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java b/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java index 20e3b5f037f..770788e774f 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java @@ -17,7 +17,10 @@ package org.springframework.boot.retry; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.function.Function; +import java.util.function.Predicate; import org.jspecify.annotations.Nullable; @@ -53,6 +56,12 @@ public final class RetryPolicySettings { */ public static final Duration DEFAULT_MAX_DELAY = Duration.ofMillis(RetryPolicy.Builder.DEFAULT_MAX_DELAY); + private List> exceptionIncludes = new ArrayList<>(); + + private List> exceptionExcludes = new ArrayList<>(); + + private @Nullable Predicate exceptionPredicate; + private Long maxAttempts = DEFAULT_MAX_ATTEMPTS; private Duration delay = DEFAULT_DELAY; @@ -72,6 +81,9 @@ public final class RetryPolicySettings { public RetryPolicy createRetryPolicy() { PropertyMapper map = PropertyMapper.get(); RetryPolicy.Builder builder = RetryPolicy.builder(); + map.from(this::getExceptionIncludes).to(builder::includes); + map.from(this::getExceptionExcludes).to(builder::excludes); + map.from(this::getExceptionPredicate).to(builder::predicate); map.from(this::getMaxAttempts).to(builder::maxAttempts); map.from(this::getDelay).to(builder::delay); map.from(this::getJitter).to(builder::jitter); @@ -80,6 +92,64 @@ public final class RetryPolicySettings { return (this.factory != null) ? this.factory.apply(builder) : builder.build(); } + /** + * Return the applicable exception types to attempt a retry for. + *

+ * The default is empty, leading to a retry attempt for any exception. + * @return the applicable exception types + */ + public List> getExceptionIncludes() { + return this.exceptionIncludes; + } + + /** + * Replace the applicable exception types to attempt a retry for by the given + * {@code includes}. Alternatively consider using {@link #getExceptionIncludes()} to + * mutate the existing list. + * @param includes the applicable exception types + */ + public void setExceptionIncludes(List> includes) { + this.exceptionIncludes = new ArrayList<>(includes); + } + + /** + * Return the non-applicable exception types to avoid a retry for. + *

+ * The default is empty, leading to a retry attempt for any exception. + * @return the non-applicable exception types + */ + public List> getExceptionExcludes() { + return this.exceptionExcludes; + } + + /** + * Replace the non-applicable exception types to attempt a retry for by the given + * {@code excludes}. Alternatively consider using {@link #getExceptionExcludes()} to + * mutate the existing list. + * @param excludes the non-applicable types + */ + public void setExceptionExcludes(List> excludes) { + this.exceptionExcludes = new ArrayList<>(excludes); + } + + /** + * Return the predicate to use to determine whether to retry a failed operation based + * on a given {@link Throwable}. + * @return the predicate to use + */ + public @Nullable Predicate getExceptionPredicate() { + return this.exceptionPredicate; + } + + /** + * Set the predicate to use to determine whether to retry a failed operation based on + * a given {@link Throwable}. + * @param exceptionPredicate the predicate to use + */ + public void setExceptionPredicate(@Nullable Predicate exceptionPredicate) { + this.exceptionPredicate = exceptionPredicate; + } + /** * Return the maximum number of retry attempts. * @return the maximum number of retry attempts diff --git a/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java b/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java index 45b6e67c0bc..29161a3c3a5 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/retry/RetryPolicySettingsTests.java @@ -17,6 +17,9 @@ package org.springframework.boot.retry; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; import org.junit.jupiter.api.Test; @@ -25,6 +28,8 @@ import org.springframework.util.backoff.BackOff; import org.springframework.util.backoff.ExponentialBackOff; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; /** @@ -34,6 +39,78 @@ import static org.mockito.Mockito.mock; */ class RetryPolicySettingsTests { + @Test + void exceptionIncludesCanBeReplaced() { + RetryPolicySettings settings = new RetryPolicySettings(); + settings.getExceptionIncludes().add(IllegalStateException.class); + settings.setExceptionIncludes(List.of(IllegalArgumentException.class)); + assertThat(settings.getExceptionIncludes()).containsExactly(IllegalArgumentException.class); + } + + @Test + void exceptionIncludesListIsCopied() { + RetryPolicySettings settings = new RetryPolicySettings(); + List> includes = new ArrayList<>(); + includes.add(IllegalStateException.class); + settings.setExceptionIncludes(includes); + includes.add(IllegalArgumentException.class); + assertThat(settings.getExceptionIncludes()).containsExactly(IllegalStateException.class); + } + + @Test + void createRetryPolicyWithExceptionIncludes() { + RetryPolicySettings settings = new RetryPolicySettings(); + settings.getExceptionIncludes().add(IllegalStateException.class); + RetryPolicy retryPolicy = settings.createRetryPolicy(); + assertThat(retryPolicy.shouldRetry(new IllegalStateException("test"))).isTrue(); + assertThat(retryPolicy.shouldRetry(new IllegalArgumentException("test"))).isFalse(); + } + + @Test + void exceptionExcludesCanBeReplaced() { + RetryPolicySettings settings = new RetryPolicySettings(); + settings.getExceptionExcludes().add(IllegalStateException.class); + settings.setExceptionExcludes(List.of(IllegalArgumentException.class)); + assertThat(settings.getExceptionExcludes()).containsExactly(IllegalArgumentException.class); + } + + @Test + void exceptionExcludesListIsCopied() { + RetryPolicySettings settings = new RetryPolicySettings(); + List> excludes = new ArrayList<>(); + excludes.add(IllegalStateException.class); + settings.setExceptionExcludes(excludes); + excludes.add(IllegalArgumentException.class); + assertThat(settings.getExceptionExcludes()).containsExactly(IllegalStateException.class); + } + + @Test + void createRetryPolicyWithExceptionExcludes() { + RetryPolicySettings settings = new RetryPolicySettings(); + settings.getExceptionExcludes().add(IllegalStateException.class); + RetryPolicy retryPolicy = settings.createRetryPolicy(); + assertThat(retryPolicy.shouldRetry(new IllegalStateException("test"))).isFalse(); + assertThat(retryPolicy.shouldRetry(new IllegalArgumentException("test"))).isTrue(); + } + + @Test + void getDefaultExceptionPredicate() { + assertThat(new RetryPolicySettings().getExceptionPredicate()).isNull(); + } + + @Test + void createRetryPolicyWithExceptionPredicate() { + IllegalArgumentException exception = new IllegalArgumentException("test"); + Predicate exceptionPredicate = mock(); + given(exceptionPredicate.test(exception)).willReturn(true); + RetryPolicySettings settings = new RetryPolicySettings(); + settings.setExceptionPredicate(exceptionPredicate); + RetryPolicy retryPolicy = settings.createRetryPolicy(); + assertThat(retryPolicy.shouldRetry(exception)).isTrue(); + then(exceptionPredicate).should().test(exception); + then(exceptionPredicate).shouldHaveNoMoreInteractions(); + } + @Test void createRetryPolicyWithDefaultsMatchesBackOffDefaults() { RetryPolicy defaultRetryPolicy = RetryPolicy.builder().build();